diff --git a/starboard/android/shared/BUILD.gn b/starboard/android/shared/BUILD.gn index fa80f7b325e6..71ad1c3f0bac 100644 --- a/starboard/android/shared/BUILD.gn +++ b/starboard/android/shared/BUILD.gn @@ -493,6 +493,10 @@ static_library("starboard_platform") { if (sb_evergreen_compatible_use_libunwind) { deps += [ "//third_party/llvm-project/libunwind:unwind_starboard" ] } + + snapshotted_media_files = [] + + sources -= snapshotted_media_files } static_library("starboard_base_symbolize") { diff --git a/starboard/android/shared/drm_system.cc b/starboard/android/shared/drm_system.cc index 7897d3b95f78..7d7d4c7431ea 100644 --- a/starboard/android/shared/drm_system.cc +++ b/starboard/android/shared/drm_system.cc @@ -68,7 +68,7 @@ SbDrmSessionRequestType SbDrmSessionRequestTypeFromMediaDrmKeyRequestType( // This has to be defined outside the above anonymous namespace to be picked up // by the comparison of std::vector. -bool operator==(const SbDrmKeyId& left, const SbDrmKeyId& right) { +inline bool operator==(const SbDrmKeyId& left, const SbDrmKeyId& right) { if (left.identifier_size != right.identifier_size) { return false; } diff --git a/starboard/shared/media_snapshot/media_snapshot.h b/starboard/shared/media_snapshot/media_snapshot.h new file mode 100644 index 000000000000..bba384092949 --- /dev/null +++ b/starboard/shared/media_snapshot/media_snapshot.h @@ -0,0 +1,27 @@ +// Copyright 2024 The Cobalt Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef STARBOARD_SHARED_MEDIA_SNAPSHOT_MEDIA_SNAPSHOT_H_ +#define STARBOARD_SHARED_MEDIA_SNAPSHOT_MEDIA_SNAPSHOT_H_ + +inline int GetMediaSnapshotVersion() { +#if SB_API_VERSION >= 15 + return 2500; +#else // SB_API_VERSION >= 15 + // Media snapshot is only support for C25 or after. + return 0; +#endif // SB_API_VERSION >= 15 +} + +#endif // STARBOARD_SHARED_MEDIA_SNAPSHOT_MEDIA_SNAPSHOT_H_ diff --git a/starboard/shared/starboard/player/player_write_samples.cc b/starboard/shared/starboard/player/player_write_samples.cc index 37c66692e443..2ae640cc3197 100644 --- a/starboard/shared/starboard/player/player_write_samples.cc +++ b/starboard/shared/starboard/player/player_write_samples.cc @@ -19,12 +19,15 @@ #if SB_API_VERSION >= 15 void SbPlayerWriteSamples(SbPlayer player, + SbMediaType sample_type, + const SbPlayerSampleInfo* sample_infos, + int number_of_sample_infos) { #else // SB_API_VERSION >= 15 void SbPlayerWriteSample2(SbPlayer player, -#endif // SB_API_VERSION >= 15 SbMediaType sample_type, const SbPlayerSampleInfo* sample_infos, int number_of_sample_infos) { +#endif // SB_API_VERSION >= 15 if (!SbPlayerIsValid(player)) { SB_LOG(WARNING) << "player is invalid."; return; diff --git a/starboard/tools/media/find_dependency.py b/starboard/tools/media/find_dependency.py new file mode 100644 index 000000000000..5eec34c28106 --- /dev/null +++ b/starboard/tools/media/find_dependency.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Cobalt Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Create a snapshot of an Starboard Android TV implementation under + 'starboard/android/shared/media_/'. This helps with running + multiple Starboard media implementations side by side.''' + +import gn_utils +import os +import source_utils +import utils + +_GN_TARGETS = [ + '//starboard/common:common', + '//starboard/android/shared:starboard_platform', + '//starboard/shared/starboard/media:media_util', + '//starboard/shared/starboard/player/filter:filter_based_player_sources', +] + + +def find_inbound_dependencies(project_root_dir, ninja_output_pathname): + project_root_dir = os.path.abspath(os.path.expanduser(project_root_dir)) + assert os.path.isdir(project_root_dir) + assert os.path.isdir(os.path.join(project_root_dir, ninja_output_pathname)) + + source_files = [] + + for target in _GN_TARGETS: + source_files += gn_utils.get_source_pathnames(project_root_dir, + ninja_output_pathname, target) + + source_files.sort() + + non_media_files = [f for f in source_files if not utils.is_media_file(f)] + + inbound_dependencies = {} + + for file in non_media_files: + with open(file, encoding='utf-8') as f: + content = f.read() + + headers = source_utils.extract_project_includes(content) + for header in headers: + if utils.is_media_file(header): + if header in inbound_dependencies: + inbound_dependencies[header].append(file) + else: + inbound_dependencies[header] = [file] + + for header, sources in inbound_dependencies.items(): + print(header) + for source in sources: + print(' ', source) + print() diff --git a/starboard/tools/media/gn_utils.py b/starboard/tools/media/gn_utils.py new file mode 100644 index 000000000000..c8a78e7d536c --- /dev/null +++ b/starboard/tools/media/gn_utils.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Cobalt Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Utility to work with gn.''' + +from datetime import datetime +from textwrap import dedent + +import os +import subprocess + +_COPYRIGHT_HEADER = '''\ + # Copyright {0} The Cobalt Authors. All Rights Reserved. + # + # Licensed under the Apache License, Version 2.0 (the "License"); + # you may not use this file except in compliance with the License. + # You may obtain a copy of the License at + # + # http://www.apache.org/licenses/LICENSE-2.0 + # + # Unless required by applicable law or agreed to in writing, software + # distributed under the License is distributed on an "AS IS" BASIS, + # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + # See the License for the specific language governing permissions and + # limitations under the License. + ''' + +_GN_CONTENT = '''########################################################## +# Configuration to extract GameActivity native files. +########################################################## + +game_activity_aar_file = "//starboard/android/apk/app/src/main/java/dev/cobalt/libraries/game_activity/games-activity-2.0.2.aar" + +game_activity_source_files = [ + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivity.cpp", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivity.h", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivityEvents.cpp", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivityEvents.h", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-activity/GameActivityLog.h", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gamecommon.h", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gametextinput.cpp", + "$target_gen_dir/game_activity/prefab/modules/game-activity/include/game-text-input/gametextinput.h", +] + +game_activity_include_dirs = + [ "$target_gen_dir/game_activity/prefab/modules/game-activity/include" ] + +action("game_activity_sources") {{ + script = "//starboard/tools/unzip_file.py" + sources = [ game_activity_aar_file ] + outputs = game_activity_source_files + args = [ + "--zip_file", + rebase_path(game_activity_aar_file, root_build_dir), + "--output_dir", + rebase_path(target_gen_dir, root_build_dir) + "/game_activity", + ] +}} + +static_library("{0}") {{ + check_includes = false + + sources = [ + {1} + ] + + configs += [ "//starboard/build/config:starboard_implementation" ] + + include_dirs = game_activity_include_dirs + + public_deps = [ + ":game_activity_sources", + "//starboard/common", + ] + deps = [ + "//third_party/opus", + ] +}} +''' + +_CANONICAL_GN_CONTENT = ''' +static_library("media_snapshot") {{ + check_includes = false + + sources = [ + {0} + ] + + configs += [ "//starboard/build/config:starboard_implementation" ] + + public_deps = [ + "//starboard/common", + ] +}} +''' + + +def _get_copyright_header(): + return dedent(_COPYRIGHT_HEADER).format(datetime.now().year) + + +def convert_source_list_to_gn_format(project_root_dir, gn_pathname, + file_pathnames): + abs_project_root_dir = os.path.abspath(project_root_dir) + abs_gn_pathname = os.path.abspath(gn_pathname) + + # The gn file should reside in the project dir. + assert abs_gn_pathname.find(abs_project_root_dir) == 0 + + source_list = [] + + for file_pathname in file_pathnames: + abs_file_pathname = os.path.abspath(file_pathname) + if os.path.dirname(file_pathname) == os.path.dirname(abs_gn_pathname): + rel_file_pathname = os.path.basename(file_pathname) + else: + rel_file_pathname = '//' + os.path.relpath(abs_file_pathname, + abs_project_root_dir) + source_list.append('\"' + rel_file_pathname + '",') + + source_list.sort() + + return source_list + + +def create_gn_file(project_root_dir, gn_pathname, library_name, file_pathnames): + abs_project_root_dir = os.path.abspath(project_root_dir) + abs_gn_pathname = os.path.abspath(gn_pathname) + + # The gn file should reside in the project dir. + assert abs_gn_pathname.find(abs_project_root_dir) == 0 + + source_list = convert_source_list_to_gn_format(project_root_dir, gn_pathname, + file_pathnames) + + with open(abs_gn_pathname, 'w+', encoding='utf-8') as f: + f.write(_get_copyright_header() + '\n' + + _GN_CONTENT.format(library_name, '\n '.join(source_list))) + + +def _get_full_pathname(project_root_dir, pathname_in_gn_format): + ''' Transform a pathname in gn format to unix format + + project_root_dir: The project root directory in unix format, e.g. + '/home/.../cobalt' + pathname_in_gn_format: A pathname in gn format, e.g. '//starboard/media.h' + return: the full path name as '/home/.../cobalt/starboard/media.h' + ''' + assert pathname_in_gn_format.find('//') == 0 + pathname_in_gn_format = pathname_in_gn_format[2:] + pathname = os.path.join(project_root_dir, pathname_in_gn_format) + if pathname.find('game-activity') < 0: + assert os.path.isfile(pathname), pathname + return pathname + + +def get_source_pathnames(project_root_dir, ninja_root_dir, target_name): + ''' Return a list of source files built for a particular ninja target + + project_root_dir: The project root directory, e.g. '/home/.../cobalt' + ninja_root_dir: The output directory, e.g. 'out/android-arm' + target_name: The name of the ninja target, e.g. '//cobalt/base:base'. + ''' + saved_python_path = os.environ['PYTHONPATH'] + os.environ['PYTHONPATH'] = os.path.abspath( + project_root_dir) + ':' + os.environ['PYTHONPATH'] + gn_desc = subprocess.check_output(['gn', 'desc', ninja_root_dir, target_name], + cwd=project_root_dir).decode('utf-8') + os.environ['PYTHONPATH'] = saved_python_path + + # gn_desc is in format: + # ... + # sources + # //path/name1 + # //path/name2 + # + # ... + lines = gn_desc.split('\n') + sources_index = lines.index('sources') + assert sources_index >= 0 + sources_index += 1 + sources = [] + while sources_index < len(lines) and lines[sources_index]: + sources.append( + _get_full_pathname(project_root_dir, lines[sources_index].strip())) + sources_index += 1 + return sources diff --git a/starboard/tools/media/snapshot.py b/starboard/tools/media/snapshot.py new file mode 100644 index 000000000000..1f8dfd91b624 --- /dev/null +++ b/starboard/tools/media/snapshot.py @@ -0,0 +1,352 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Cobalt Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Create a snapshot of an Starboard Android TV implementation under + 'starboard/android/shared/media_snapshot_/'. This helps with + running multiple Starboard media implementations side by side.''' + +import gn_utils +import os +import source_utils +import utils + + +class Snapshot: + ''' Snapshot an SbPlayer implementation on Android TV + + It creates a snapshot of an SbPlayer implementation on Android TV + specified by 'source_project_root_dir' and 'ninja_output_pathname' into + '/starboard/android/shared/ + '. + + The snapshot can be used side by side with the default SbPlayer + implementation on Android TV of ''.''' + + _GN_TARGETS = [ + '//starboard/common:common', + '//starboard/android/shared:starboard_platform', + '//starboard/shared/starboard/media:media_util', + '//starboard/shared/starboard/player/filter:filter_based_player_sources', + ] + + _DESTINATION_SUB_DIR = 'starboard/android/shared' + + def __init__(self, source_project_root_dir, destination_project_root_dir, + ninja_output_pathname, media_snapshot_version): + assert isinstance(media_snapshot_version, str) + + self.source_project_root_dir = os.path.abspath( + os.path.expanduser(source_project_root_dir)) + self.destination_project_root_dir = os.path.abspath( + os.path.expanduser(destination_project_root_dir)) + self.ninja_output_pathname = ninja_output_pathname + self.media_snapshot_version = media_snapshot_version + + assert os.path.isdir(self.source_project_root_dir) + assert os.path.isdir( + os.path.join(self.source_project_root_dir, self.ninja_output_pathname)) + assert os.path.isdir(self.destination_project_root_dir) + # The snapshot process may modify some of the source files. Requiring a + # separate checkout as source folder to avoid accidentally snapshotting + # modified source files. + assert not os.path.samefile(self.source_project_root_dir, + self.destination_project_root_dir) + # Some of the logic relies on snapshotted files are inside a subfolder + # containing 'media_snapshot/'. This will break if the root dir of the + # destination project happens to contain 'media_snapshot/'. + assert self.destination_project_root_dir.find('media_snapshot/') == -1 + + def _get_destination_pathname(self, source_pathname): + # If the source is 'starboard/shared/starboard/media/media_util.cc', the + # destination will be + # 'starboard/android/shared/media_snapshot//media/media_util.cc'. + # Note that the leading 'starboard/' of the source will be removed. + source_rel_pathname = os.path.relpath(source_pathname, + self.source_project_root_dir) + assert source_rel_pathname.find('starboard' + os.path.sep) == 0 + source_rel_pathname = source_rel_pathname[len('starboard') + 1:] + + return os.path.join(self.destination_project_root_dir, + self._DESTINATION_SUB_DIR, + 'media_snapshot/' + self.media_snapshot_version, + source_rel_pathname) + + def snapshot(self): + self.try_create_canonical_sb_implementation() + + source_files = [] + + for target in self._GN_TARGETS: + source_files += gn_utils.get_source_pathnames( + self.source_project_root_dir, self.ninja_output_pathname, target) + + source_files = [f for f in source_files if utils.is_media_file(f)] + + source_files.sort() + + destination_files = [] + + class_names = [] + headers_dict = {} + self.is_source_c24 = False + + for source_file in source_files: + destination_file = self._get_destination_pathname(source_file) + if source_utils.is_header_file(source_file): + headers_dict[os.path.relpath( + source_file, self.source_project_root_dir)] = os.path.relpath( + destination_file, self.destination_project_root_dir) + with open(source_file, encoding='utf-8') as f: + content = f.read() + # SbTime is deprecated in C25, use it as a hint to determine whether + # the source is C24. + if content.find(' SbTime ') >= 0: + self.is_source_c24 = True + class_names += source_utils.extract_class_or_struct_names(content) + + destination_files.append(destination_file) + + class_names.sort() + + for i in range(0, len(source_files)): + self.snapshot_file(source_files[i], destination_files[i], class_names, + headers_dict) + + destination_files.sort() + self._create_snapshot_gn_file(destination_files) + + android_gn_pathname = os.path.join(self.destination_project_root_dir, + 'starboard/android/shared/BUILD.gn') + android_gn_content = utils.read_file(android_gn_pathname) + + assert android_gn_content.find( + '"//starboard/android/shared/media_snapshot",') != -1 + + gn_target = ('//starboard/android/shared/media_snapshot/' + + self.media_snapshot_version) + + # Check if there is already a reference + if android_gn_content.find(gn_target) == -1: + android_gn_content = android_gn_content.replace( + '"//starboard/android/shared/media_snapshot",', + '"//starboard/android/shared/media_snapshot",\n "' + gn_target + + '",') + + utils.write_file(android_gn_pathname, android_gn_content) + + self.amend_canonical_sb_implementations() + + def try_create_canonical_sb_implementation(self): + android_gn_pathname = os.path.join(self.destination_project_root_dir, + 'starboard/android/shared/BUILD.gn') + canonical_snapshot_dir = os.path.join(self.destination_project_root_dir, + self._DESTINATION_SUB_DIR, + 'media_snapshot/') + + android_gn_content = utils.read_file(android_gn_pathname) + + # Ensure that we are operating on the correct file + assert android_gn_content.find('":starboard_base_symbolize",') != -1 + assert android_gn_content.find('snapshotted_media_files = [') != -1 + + if android_gn_content.find('snapshotted_media_files = []') == -1: + print('Canonical snapshot already created.') + assert os.path.isfile( + os.path.join(canonical_snapshot_dir, 'audio_sink_create.cc')) + return + + print('Creating canonical snapshot ...') + sb_implementation_files = [] + + for target in self._GN_TARGETS: + for pathname in gn_utils.get_source_pathnames( + self.destination_project_root_dir, self.ninja_output_pathname, + target): + if not utils.is_media_file(pathname): + continue + # The canonical implementation is based on the primary implementation + if pathname.find('media_snapshot/') != -1: + continue + + content = utils.read_file(pathname) + if source_utils.is_sb_implementation_file(pathname, content): + sb_implementation_files.append(pathname) + + # We haven't snapshotted yet, `sb_implementation_files` shouldn't be empty + assert len(sb_implementation_files) > 0 + + # Now create the canonical implementation inside + # 'starboard/android/shared/media_snapshot'. Note that any non-canonical + # snapshot will be put into a subfolder of it. + source_file_pathnames = [] + for pathname in sb_implementation_files: + destination_pathname = os.path.join(canonical_snapshot_dir, + os.path.basename(pathname)) + source_file_pathnames.append(destination_pathname) + source_utils.create_canonical_file(self.destination_project_root_dir, + pathname, destination_pathname) + + # Create a gn file referring to the above source files + gn_utils.create_gn_file(self.destination_project_root_dir, + os.path.join(canonical_snapshot_dir + 'BUILD.gn'), + 'media_snapshot', source_file_pathnames) + + # Exclude the original source files from 'starboard/android/shared/BUILD.gn' + sb_implementation_files = gn_utils.convert_source_list_to_gn_format( + self.destination_project_root_dir, android_gn_pathname, + sb_implementation_files) + android_gn_content = android_gn_content.replace( + 'snapshotted_media_files = []', 'snapshotted_media_files = [\n ' + + '\n '.join(sb_implementation_files) + '\n ]') + + # Add reference to canonical snapshot + android_gn_content = android_gn_content.replace( + '":starboard_base_symbolize",', + ('":starboard_base_symbolize",\n' + + ' "//starboard/android/shared/media_snapshot",')) + + utils.write_file(android_gn_pathname, android_gn_content) + + def amend_canonical_sb_implementations(self): + sb_implementation_files = [] + + for pathname in gn_utils.get_source_pathnames( + self.destination_project_root_dir, self.ninja_output_pathname, + '//starboard/android/shared/media_snapshot'): + if not utils.is_media_file(pathname): + continue + # We only amend the canonical implementation + if pathname.find('media_snapshot/') == -1: + continue + + content = utils.read_file(pathname) + if source_utils.is_sb_implementation_file(pathname, content): + sb_implementation_files.append(pathname) + + assert len(sb_implementation_files) > 0 + print(sb_implementation_files) + + for pathname in sb_implementation_files: + with open(pathname, encoding='utf-8') as f: + content = f.read() + content = source_utils.patch_sb_implementation_with_branching_call( + pathname, content, self.media_snapshot_version) + with open(pathname, 'w+', encoding='utf-8') as f: + f.write(content) + + def snapshot_file(self, source_pathname, destination_pathname, class_names, + headers_dict): + print('snapshotting', source_pathname, '=>', destination_pathname) + + if (self.is_source_c24 and + source_utils.is_trivially_modified_c24_file(source_pathname)): + # These files are trivially modified between C24 and C25, and they + # contain changes hard to replace automatically (e.g. SbThreadCreate()). + # Use the destination implementation of these files instead, and we should + # manually diff these files offline to ensure that such replacement is + # safe. + rel_pathname = os.path.relpath(source_pathname, + self.source_project_root_dir) + canonical_pathname = os.path.join(self.destination_project_root_dir, + rel_pathname) + with open(canonical_pathname, encoding='utf-8') as f: + content = f.read() + else: + with open(source_pathname, encoding='utf-8') as f: + content = f.read() + + if utils.is_header_file(source_pathname): + source_macro = source_utils.generate_include_guard_macro( + self.source_project_root_dir, source_pathname) + destination_macro = source_utils.generate_include_guard_macro( + self.destination_project_root_dir, destination_pathname) + assert content.find(source_macro) > 0 + content = content.replace(source_macro, destination_macro) + assert content.find(source_macro) < 0 + + if not os.path.isdir(os.path.dirname(destination_pathname)): + os.makedirs(os.path.dirname(destination_pathname)) + + for source in headers_dict: + content = content.replace('#include "' + source + '"', + '#include "' + headers_dict[source] + '"') + + content = content.replace('namespace shared', + 'namespace shared_' + self.media_snapshot_version) + + for class_name in class_names: + content = source_utils.replace_class_under_namespace( + content, class_name, 'shared', + 'shared_' + self.media_snapshot_version) + + for symbol_name in [ + 'AudioDurationToFrames', 'AudioFramesToDuration', + 'CanPlayMimeAndKeySystem', 'ErrorCB', 'EndedCB', + 'GetAudioConfiguration', 'GetBytesPerSample', + 'GetMaxVideoInputSizeForCurrentThread', 'IsSDRVideo', 'IsWidevineL1', + 'IsWidevineL3', 'PrerolledCB', 'SetMaxVideoInputSizeForCurrentThread' + ]: + content = source_utils.replace_class_under_namespace( + content, symbol_name, 'shared', + 'shared_' + self.media_snapshot_version) + + content = content.replace( + 'starboard::shared::starboard::player', 'starboard::shared_' + + self.media_snapshot_version + '::starboard::player') + + # The following replacements are very specific. Including here so we don't + # have to modify the generated code manually. + content = content.replace(' ThreadChecker ', + ' shared::starboard::ThreadChecker ') + content = content.replace( + ' Application::Get', + ' ::starboard::shared::starboard::Application::Get') + content = content.replace('worker_ = starboard::make_scoped_ptr(', + 'worker_.reset(') + + content = content.replace('#include "third_party/opus/include', + '#include "third_party/opus/src/include') + + content = source_utils.add_namespace(content, 'JniEnvExt', + '::starboard::android::shared') + content = source_utils.add_namespace(content, 'ScopedJavaByteBuffer', + '::starboard::android::shared') + content = source_utils.add_namespace(content, 'ScopedLocalJavaRef', + '::starboard::android::shared') + + # Replace JNI calls + for jni_prefix in [ + 'Java_dev_cobalt_media_MediaCodecBridge', + 'Java_dev_cobalt_media_MediaDrmBridge' + ]: + content = content.replace(jni_prefix, + jni_prefix + self.media_snapshot_version) + + if source_utils.is_sb_implementation_file(source_pathname, content): + content = source_utils.append_suffix_to_sb_function( + source_pathname, content, self.media_snapshot_version) + + with open(destination_pathname, 'w+', encoding='utf-8') as f: + f.write(source_utils.update_starboard_usage(content)) + + def _create_snapshot_gn_file(self, file_pathnames): + gn_utils.create_gn_file( + self.destination_project_root_dir, + self._get_destination_pathname( + os.path.join(self.source_project_root_dir, 'starboard/BUILD.gn')), + self.media_snapshot_version, file_pathnames) + + +snapshot = Snapshot('~/cobalt_c25', '~/cobalt', 'out/android-arm_devel', '2500') +snapshot.snapshot() diff --git a/starboard/tools/media/source_utils.py b/starboard/tools/media/source_utils.py new file mode 100644 index 000000000000..fbf547956f91 --- /dev/null +++ b/starboard/tools/media/source_utils.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Cobalt Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +'''Utility functions to manipulate source file.''' + +from datetime import datetime +from textwrap import dedent + +import os +import re +import utils + +_COPYRIGHT_HEADER = '''\ + // Copyright {0} The Cobalt Authors. All Rights Reserved. + // + // Licensed under the Apache License, Version 2.0 (the "License"); + // you may not use this file except in compliance with the License. + // You may obtain a copy of the License at + // + // http://www.apache.org/licenses/LICENSE-2.0 + // + // Unless required by applicable law or agreed to in writing, software + // distributed under the License is distributed on an "AS IS" BASIS, + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + // See the License for the specific language governing permissions and + // limitations under the License. + ''' + +_CANONICAL_FILE_CONTENT = '''\ + + {0} + + extern "C" {{ + + SB_EXPORT {1}; + + }} // extern "C" + + {2} {{ + return {3}; + }} + + #define {4} {5} + #define REFERENCE_SOURCE_FILE "{6}" + + #include REFERENCE_SOURCE_FILE + ''' + + +def _get_copyright_header(): + return dedent(_COPYRIGHT_HEADER).format(datetime.now().year) + + +# for `player_create.cc', return 'starboard/player.h'. +def _get_starboard_includes_and_usings(pathname): + basename = os.path.basename(pathname) + + if (basename in [ + 'media_is_audio_supported.cc', 'media_is_supported.cc', + 'media_is_video_supported.cc' + ]): + return dedent('''\ + #include "starboard/shared/starboard/media/media_support_internal.h" + + using starboard::shared::starboard::media::MimeType;''') + + if basename.find('audio_sink_') == 0: + return '#include "starboard/audio_sink.h"' + if basename.find('decode_target_') == 0: + return '#include "starboard/decode_target.h"' + if basename.find('drm_') == 0: + return '#include "starboard/drm.h"' + if basename.find('media_') == 0: + return '#include "starboard/media.h"' + if basename.find('player_') == 0: + return '#include "starboard/player.h"' + assert False + + +# starboard/player.h => STARBOARD_PLAYER_H_ +def generate_include_guard_macro(project_root_dir, file_pathname): + rel_pathname = os.path.relpath(file_pathname, project_root_dir) + return rel_pathname.replace('/', '_').replace('.', '_').upper() + '_' + + +def extract_class_or_struct_names(content): + pattern = r'^\s*class\s+(\w+)\s*[:{]' + matches = re.findall(pattern, content, re.MULTILINE) + + pattern = r'^\s*struct\s+(\w+)\s*[:{]' + return matches + re.findall(pattern, content, re.MULTILINE) + + +def extract_project_includes(content): + pattern = r'#\s*include\s*"([^"]+)"' + return re.findall(pattern, content, re.MULTILINE) + + +# This replace 'JNIEnv' to the content of new_text, but won't replace +# 'shared::JNIEnv'. +def replace_class_name_without_namespace(content, class_name, new_text): + pattern = r'(? 0 + if namespace[-2:] != '::': + namespace += '::' + return replace_class_name_without_namespace(content, class_name, + namespace + class_name) + + +def replace_class_under_namespace(content, class_name, old_namespace, + new_namespace): + pattern = rf'{old_namespace}::(\S*)?{class_name}' + replacement = rf'{new_namespace}::\1{class_name}' + return re.sub(pattern, replacement, content) + + +def is_header_file(file_pathname): + return file_pathname[-2:] == '.h' + + +def add_header_file(content, header_file): + is_project_header = header_file.find('/') >= 0 + + if is_project_header: + include_directive = '#include "' + header_file + '"' + include_directive_prefix = '#include "' + search_func = str.rfind + else: + include_directive = '#include <' + header_file + '>' + include_directive_prefix = '#include <' + if header_file.find('.'): + search_func = str.find # C header files first + else: + search_func = str.rfind # C++ header files second + + if content.find(include_directive) >= 0: + # Already included + return content + + index = search_func(content, include_directive_prefix) + if index >= 0: + index = content.find('\n', index) + return content[:index] + '\n' + include_directive + '\n' + content[index:] + + # Cannot find the group, add it to the very beginning and refine it manually + return include_directive + '\n' + content + + +def is_trivially_modified_c24_file(input_pathname): + pathnames = [ + 'starboard/android/shared/audio_sink_min_required_frames_tester.cc', + 'starboard/android/shared/audio_track_audio_sink_type.cc', + 'starboard/android/shared/media_decoder.cc', + 'starboard/android/shared/video_max_video_input_size.cc', + 'starboard/shared/starboard/audio_sink/stub_audio_sink_type.cc', + 'starboard/shared/starboard/player/job_queue.cc', + 'starboard/shared/starboard/player/job_thread.cc', + 'starboard/shared/starboard/player/player_worker.cc', + ] + for pathname in pathnames: + if input_pathname.find(pathname) > 0: + return True + + return False + + +def is_sb_implementation_file(pathname, content): + basename, ext = utils.get_base_file_name_and_ext(pathname) + if ext != 'cc': + return False + + function_name = utils.base_name_to_sb_function_name(basename) + if content.find(' ' + function_name + '(') == -1: + return False + return True + + +def append_suffix_to_sb_function(pathname, content, suffix): + print('appending suffix for Sb function to', pathname) + + basename, ext = utils.get_base_file_name_and_ext(pathname) + assert ext == 'cc' + + function_name = utils.base_name_to_sb_function_name(basename) + + assert content.find(' ' + function_name + '(') != -1 + + return content.replace(function_name, function_name + suffix) + + +# For the content of player_destroy.cc and 'SbPlayerDestroy', returns +# 'void SbPlayerDestroy(SbPlayer player)'. +def get_function_prototype(content, function_name): + # We assume the first occurrence is the prototype. This won't work when + # there is a comment before the prototype, which isn't currently used. + left = content.find(' ' + function_name + '(') + + # Move to right after the previous line end + left = content.rfind('\n', 0, left) + 1 + assert left > 0 + + # Now keep find until the next ')' + right = content.find(')', left) + assert right != -1 + + return content[left:right + 1] + + +def patch_sb_implementation_with_branching_call(pathname, content, suffix): + print('patching', pathname) + + basename, ext = utils.get_base_file_name_and_ext(pathname) + assert ext == 'cc' + + function_name = utils.base_name_to_sb_function_name(basename) + + if content.find(function_name + suffix) != -1: + # Already patched, don't patch further. + return content + + assert content.find(' ' + function_name + '(') != -1 + + prototype = get_function_prototype(content, function_name) + + branched_prototype = prototype.replace(function_name, function_name + suffix) + + content = add_header_file(content, + 'starboard/shared/media_snapshot/media_snapshot.h') + + offset = content.find(prototype) + content = content[:offset] + branched_prototype + ';\n\n' + content[offset:] + + offset = content.find(prototype) + offset = content.find('{', offset) + assert offset != -1 + offset += 1 + + calling_statement = utils.get_calling_statement_from_prototype( + branched_prototype) + if_statement = ('\n' + ' if (GetMediaSnapshotVersion() == ' + suffix + + ') {\n' + ' return ' + calling_statement + ';\n' + ' }\n') + + return content[:offset] + if_statement + content[offset:] + + +def create_canonical_file(source_project_root_dir, source_pathname, + destination_pathname): + assert os.path.basename(source_pathname) == os.path.basename( + destination_pathname) + + with open(source_pathname, encoding='utf-8') as f: + content = f.read() + + basename, ext = utils.get_base_file_name_and_ext(source_pathname) + assert ext == 'cc' + + # '.../decode_target_release.cc' => SbDecodeTargetRelease + function_name = utils.base_name_to_sb_function_name(basename) + + assert content.find(' ' + function_name + '(') != -1 + + # SbDecodeTargetRelease => starboard/decode_target.h + includes_and_usings = _get_starboard_includes_and_usings(source_pathname) + + # SbDecodeTargetRelease => + # void SbDecodeTargetRelease(SbDecodeTarget decode_target) + prototype = get_function_prototype(content, function_name) + + canonical_function_name = function_name + 'Canonical' + + # void SbDecodeTargetRelease(SbDecodeTarget decode_target) => + # void SbDecodeTargetReleaseCanonical(SbDecodeTarget decode_target) + canonical_prototype = prototype.replace(function_name, + canonical_function_name) + + # void SbDecodeTargetReleaseCanonical(SbDecodeTarget decode_target) => + # SbDecodeTargetReleaseCanonical(decode_target) + calling_statement = utils.get_calling_statement_from_prototype( + canonical_prototype) + + source_rel_pathname = os.path.relpath(source_pathname, + source_project_root_dir) + with open(destination_pathname, 'w+', encoding='utf-8') as f: + f.write(_get_copyright_header() + dedent(_CANONICAL_FILE_CONTENT).format( + includes_and_usings, canonical_prototype, prototype, calling_statement, + function_name, canonical_function_name, source_rel_pathname)) + + +def update_starboard_usage(source_content): + # The function can be further optimized using regex to conduct multiple + # replaces at once, but the current implementation (i.e. replacing multiple + # times) works. + + # SbTime + source_content = re.sub(r'\bSbTime\b', 'int64_t', source_content) + source_content = re.sub(r'\bSbTimeMonotonic\b', 'int64_t', source_content) + source_content = re.sub(r'\bkSbTimeMillisecond\b', '1000LL', source_content) + source_content = re.sub(r'\bkSbTimeNanosecondsPerMicrosecond\b', '1000LL', + source_content) + source_content = re.sub(r'\bkSbTimeSecond\b', '1\'000\'000LL', source_content) + if re.search(r'\bSbTimeGetMonotonicNow\b', source_content): + source_content = re.sub(r'\bSbTimeGetMonotonicNow\b', + '::starboard::CurrentMonotonicTime', source_content) + source_content = add_header_file(source_content, 'starboard/common/time.h') + + source_content = source_content.replace('kSbTimeMax', 'kSbInt64Max') + source_content = source_content.replace('kSbTimeDay', 'kSbInt64Max') + source_content = source_content.replace('#include "starboard/time.h"\n', '') + + # SbThread + source_content = re.sub(r'\bSbThread\b', 'pthread_t', source_content) + source_content = re.sub(r'\bkSbThreadInvalid\b', '0', source_content) + source_content = re.sub(r'SbThreadIsValid\((.*?)\)', r'((\1) != 0)', + source_content) + source_content = source_content.replace('SbThreadJoin(', 'pthread_join(') + if re.search(r'\bSbThreadSleep\b', source_content): + source_content = re.sub(r'\bSbThreadSleep\b', 'usleep', source_content) + source_content = add_header_file(source_content, 'unistd.h') + source_content = source_content.replace('SbThreadYield(', 'sched_yield(') + + # SbThreadLocal + source_content = source_content.replace('kSbThreadLocalKeyInvalid', '0') + source_content = source_content.replace('SbThreadLocalKey', 'pthread_key_t') + source_content = source_content.replace('SbThreadSetLocalValue', + 'pthread_setspecific') + + # scoped_ptr<> and scoped_array<> + if source_content.find('scoped_ptr') != -1: + source_content = source_content.replace( + '#include "starboard/common/scoped_ptr.h"\n', '') + source_content = add_header_file(source_content, 'memory') + + # The following replacement is very specific. Including here so we don't have + # to modify the generated code manually. + source_content = source_content.replace( + 'return audio_decoder_impl.PassAs();', + ('return std::unique_ptr(audio_decoder_impl.' + + 'release());')) + + source_content = re.sub(r'scoped_ptr<(\w*)>\(NULL\)', + r'std::unique_ptr<\1>()', source_content) + source_content = re.sub(r'\bstarboard::scoped_ptr\b', 'std::unique_ptr', + source_content) + source_content = re.sub(r'\bstarboard::make_scoped_ptr\b', 'std::unique_ptr', + source_content) + source_content = re.sub(r'\bmake_scoped_ptr\b', 'std::unique_ptr', + source_content) + source_content = re.sub(r'\bscoped_ptr\b', 'std::unique_ptr', source_content) + source_content = re.sub(r'\b(\w*)\.Pass\(\)', r'std::move(\1)', + source_content) + source_content = re.sub(r'scoped_array<(\w*)>', r'std::unique_ptr<\1[]>', + source_content) + + # SbOnce + source_content = source_content.replace('#include "starboard/once.h"', + '#include "starboard/common/once.h"') + source_content = source_content.replace('SbOnceControl', 'pthread_once_t') + source_content = source_content.replace('SbOnce(', 'pthread_once(') + source_content = source_content.replace('SB_ONCE_INITIALIZER', + 'PTHREAD_ONCE_INIT') + + # string ops + source_content = source_content.replace('SbStringScanF', 'sscanf') + source_content = source_content.replace('SbStringCompareNoCase', 'strcasecmp') + + # reset_and_return.h + source_content = source_content.replace( + '#include "starboard/common/reset_and_return.h"\n', '') + source_content = source_content.replace( + 'using common::ResetAndReturn;', + ('template \nT ResetAndReturn(T* t) {\n' + + ' T result(std::move(*t));\n *t = T();\n return result;\n}')) + + return source_content diff --git a/starboard/tools/media/utils.py b/starboard/tools/media/utils.py new file mode 100644 index 000000000000..ac270494271f --- /dev/null +++ b/starboard/tools/media/utils.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +# Copyright 2024 The Cobalt Authors. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Utility functions.""" + +import os + + +def is_media_file(pathname): + # Image decoder and media session are not media + if pathname.find('image') >= 0 or pathname.find('media_session') >= 0: + return False + + # While these two are media files, they are unused on Android TV and cause + # build errors. + if pathname.find('media_get_buffer_storage_type.cc') >= 0: + return False + if pathname.find('punchout_video_renderer_sink.cc') >= 0: + return False + + # These files should be the same among media implementations, and should be + # manually consolidated if there are any differences + # TODO: Print them to log if there are any differences + if (pathname.find('player_create.cc') >= 0 or + pathname.find('player_destroy.cc') >= 0): + return False + + # Assume the function in starboard/common are the same across all versions. + # Exclude them to avoid the extra handling required as they are under the root + # starboard namespace and the functions are called directly without any + # namespace specifier. + if pathname.find('starboard/common/') >= 0: + return False + + if (pathname.find('starboard/shared/starboard/audio_sink') >= 0 or + pathname.find('starboard/shared/starboard/decode_target') >= 0 or + pathname.find('starboard/shared/starboard/drm') >= 0 or + pathname.find('starboard/shared/starboard/media') >= 0 or + pathname.find('starboard/shared/starboard/opus') >= 0 or + pathname.find('starboard/shared/starboard/player') >= 0 or + pathname.find('starboard/shared/starboard/drm') >= 0): + return True + + return (pathname.find('audio') >= 0 or pathname.find('decode') >= 0 or + pathname.find('drm') >= 0 or pathname.find('media') >= 0 or + pathname.find('player') >= 0 or pathname.find('video') >= 0) + + +def is_header_file(pathname): + return pathname[-2:] == '.h' + + +# '.../starboard/android/shared/player_create.cc' => 'player_create', 'cc' +def get_base_file_name_and_ext(pathname): + # '.../starboard/android/shared/player_create.cc' => player_create.cc + basename = os.path.basename(pathname) + + return basename.split('.') + + +def base_name_to_sb_function_name(pathname): + # 'player_create' => 'SbPlayerCreate' + return 'Sb' + ''.join(x.capitalize() for x in pathname.split('_')) + + +# For prototype +# const void* SbDrmGetMetrics(SbDrmSystem drm_system, int* size) +# returns +# SbDrmGetMetrics(drm_system, size) +# i.e. remove the return type, and all types of parameters. +def get_calling_statement_from_prototype(prototype): + # multiline to single line + calling_statement = prototype.replace('\n', '') + calling_statement = calling_statement.strip() + + # const void* SbDrmGetMetrics(SbDrmSystem drm_system, int* size) => + # SbDrmGetMetrics(SbDrmSystem drm_system, int* size) + assert calling_statement.find(' Sb') != -1 + calling_statement = calling_statement[calling_statement.find(' Sb') + 1:] + + # SbDrmGetMetrics(SbDrmSystem drm_system, int* size) => + # SbDrmGetMetrics(drm_system, size) + assert calling_statement[-1] == ')' + calling_statement = calling_statement[:-1] + name, parameters = calling_statement.split('(') + + arguments = [] + for parameter in parameters.split(','): + arguments.append(parameter.split(' ')[-1]) + + return name + '(' + ', '.join(arguments) + ')' + + +def read_file(pathname): + with open(pathname, encoding='utf-8') as f: + return f.read() + + +def write_file(pathname, content): + with open(pathname, 'w+', encoding='utf-8') as f: + f.write(content)