From aa1f8c80739b03cbfbe2f624c29e60c7cf1aaac4 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 1 Sep 2023 17:20:42 -0700 Subject: [PATCH 001/140] Cherry pick PR #1385: [android] Enable filtered partial audio tests (#1472) Refer to the original PR: https://github.com/youtube/cobalt/pull/1385 The cl also fixed a multi thread issue of AudioFrameDiscarder and enhanced timestamp checks of audio decoder outputs. b/274021285 b/289281412 b/292409536 b/298072842 Co-authored-by: Jason <2697724+jasonzhangxx@users.noreply.github.com> --- starboard/android/shared/test_filters.py | 6 --- .../player/filter/audio_frame_discarder.cc | 53 +++++++++++-------- .../player/filter/audio_frame_discarder.h | 4 +- .../filter/testing/audio_decoder_test.cc | 8 +++ 4 files changed, 42 insertions(+), 29 deletions(-) diff --git a/starboard/android/shared/test_filters.py b/starboard/android/shared/test_filters.py index 641ae2be1e3d..ae8fbb67a358 100644 --- a/starboard/android/shared/test_filters.py +++ b/starboard/android/shared/test_filters.py @@ -86,15 +86,9 @@ # TODO: Filter this test on a per-device basis. 'SbMediaCanPlayMimeAndKeySystem.MinimumSupport', - # TODO: b/289281412 Make this test work on lab devices consistently. - 'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.PartialAudio/*', - # TODO: b/292319097 Make this test work on lab devices consistently. 'SbPlayerTest.MaxVideoCapabilities', - # TODO: b/292409536 Make this test fork on lab devices consistently. - 'SbPlayerWriteSampleTests/SbPlayerWriteSampleTest.PartialAudioDiscardAll/*', - # TODO: b/280432564 Make this test work on lab devices consistently. 'SbAudioSinkTest.ContinuousAppend', ], diff --git a/starboard/shared/starboard/player/filter/audio_frame_discarder.cc b/starboard/shared/starboard/player/filter/audio_frame_discarder.cc index 7b8d1922d672..b1ad4fb2304e 100644 --- a/starboard/shared/starboard/player/filter/audio_frame_discarder.cc +++ b/starboard/shared/starboard/player/filter/audio_frame_discarder.cc @@ -23,13 +23,7 @@ namespace player { namespace filter { void AudioFrameDiscarder::OnInputBuffers(const InputBuffers& input_buffers) { - if (input_buffer_infos_.size() >= kMaxNumberOfPendingInputBufferInfos) { - // This shouldn't happen as it's DCHECKed at the end of this function. Add - // an extra check here to ensure that |input_buffer_infos_| won't grow - // without bound, which can lead to OOM in production. - return; - } - + ScopedLock lock(mutex_); for (auto&& input_buffer : input_buffers) { SB_DCHECK(input_buffer); SB_DCHECK(input_buffer->sample_type() == kSbMediaTypeAudio); @@ -41,6 +35,8 @@ void AudioFrameDiscarder::OnInputBuffers(const InputBuffers& input_buffers) { }); } + // Add a DCheck here to ensure that |input_buffer_infos_| won't grow + // without bound, which can lead to OOM. SB_DCHECK(input_buffer_infos_.size() < kMaxNumberOfPendingInputBufferInfos); } @@ -49,32 +45,46 @@ void AudioFrameDiscarder::AdjustForDiscardedDurations( scoped_refptr* decoded_audio) { SB_DCHECK(decoded_audio); SB_DCHECK(*decoded_audio); - // TODO: Comment out the SB_DCHECK due to b/274021285. We can re-enable it - // after b/274021285 is resolved. - // SB_DCHECK(!input_buffer_infos_.empty()); - if (input_buffer_infos_.empty()) { - SB_LOG(WARNING) << "Inconsistent number of audio decoder outputs. Received " - "outputs when input buffer list is empty."; - return; + InputBufferInfo input_info; + { + ScopedLock lock(mutex_); + SB_DCHECK(!input_buffer_infos_.empty()); + + if (input_buffer_infos_.empty()) { + SB_LOG(WARNING) + << "Inconsistent number of audio decoder outputs. Received " + "outputs when input buffer list is empty."; + return; + } + + input_info = input_buffer_infos_.front(); + input_buffer_infos_.pop(); } - auto info = input_buffer_infos_.front(); - SB_LOG_IF(WARNING, info.timestamp != (*decoded_audio)->timestamp()) - << "Inconsistent timestamps between InputBuffer (@" << info.timestamp - << ") and DecodedAudio (@" << (*decoded_audio)->timestamp() << ")."; - input_buffer_infos_.pop(); + // We accept a small offset due to the precision of computation. If the + // outputs have different timestamps than inputs, discarded durations will be + // ignored. + const SbTimeMonotonic kTimestampOffset = 10; + if (std::abs(input_info.timestamp - (*decoded_audio)->timestamp()) > + kTimestampOffset) { + SB_LOG(WARNING) << "Inconsistent timestamps between InputBuffer (@" + << input_info.timestamp << ") and DecodedAudio (@" + << (*decoded_audio)->timestamp() << ")."; + return; + } (*decoded_audio) ->AdjustForDiscardedDurations(sample_rate, - info.discarded_duration_from_front, - info.discarded_duration_from_back); + input_info.discarded_duration_from_front, + input_info.discarded_duration_from_back); // `(*decoded_audio)->frames()` might be 0 here. We don't set it to nullptr // in this case so the DecodedAudio instance is always valid (but might be // empty). } void AudioFrameDiscarder::OnDecodedAudioEndOfStream() { + ScopedLock lock(mutex_); // |input_buffer_infos_| can have extra elements when the decoder skip outputs // due to errors (like invalid inputs). SB_LOG_IF(INFO, !input_buffer_infos_.empty()) @@ -83,6 +93,7 @@ void AudioFrameDiscarder::OnDecodedAudioEndOfStream() { } void AudioFrameDiscarder::Reset() { + ScopedLock lock(mutex_); input_buffer_infos_ = std::queue(); } diff --git a/starboard/shared/starboard/player/filter/audio_frame_discarder.h b/starboard/shared/starboard/player/filter/audio_frame_discarder.h index a1f4f3776eed..80f5fbeb2e74 100644 --- a/starboard/shared/starboard/player/filter/audio_frame_discarder.h +++ b/starboard/shared/starboard/player/filter/audio_frame_discarder.h @@ -17,6 +17,7 @@ #include +#include "starboard/common/mutex.h" #include "starboard/common/ref_counted.h" #include "starboard/shared/internal_only.h" #include "starboard/shared/starboard/player/decoded_audio_internal.h" @@ -35,8 +36,6 @@ namespace filter { // corresponding InputBuffer object isn't available at the time. // This class assumes that there is exact one DecodedAudio object produced for // one InputBuffer object, which may not always be the case. -// TODO(b/274021285): Ensure that the class works when there isn't a 1:1 -// relationship between DecodedAudio and InputBuffer. class AudioFrameDiscarder { public: void OnInputBuffers(const InputBuffers& input_buffers); @@ -55,6 +54,7 @@ class AudioFrameDiscarder { static constexpr size_t kMaxNumberOfPendingInputBufferInfos = 128; + Mutex mutex_; std::queue input_buffer_infos_; }; diff --git a/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc b/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc index b9500dff5e97..4b233210444a 100644 --- a/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc +++ b/starboard/shared/starboard/player/filter/testing/audio_decoder_test.cc @@ -185,6 +185,7 @@ class AudioDecoderTest last_input_buffer_ = GetAudioInputBuffer(index); audio_decoder_->Decode({last_input_buffer_}, consumed_cb()); + written_inputs_.push_back(last_input_buffer_); } void WriteSingleInput(size_t index, @@ -200,6 +201,7 @@ class AudioDecoderTest last_input_buffer_ = GetAudioInputBuffer( index, discarded_duration_from_front, discarded_duration_from_back); audio_decoder_->Decode({last_input_buffer_}, consumed_cb()); + written_inputs_.push_back(last_input_buffer_); } // This has to be called when OnOutput() is called. @@ -228,6 +230,11 @@ class AudioDecoderTest ASSERT_LT(decoded_audios_.back()->timestamp(), local_decoded_audio->timestamp()); } + if (!using_stub_decoder_ && invalid_inputs_.empty()) { + ASSERT_NEAR(local_decoded_audio->timestamp(), + written_inputs_.front()->timestamp(), 5); + written_inputs_.pop_front(); + } decoded_audios_.push_back(local_decoded_audio); *decoded_audio = local_decoded_audio; } @@ -428,6 +435,7 @@ class AudioDecoderTest bool can_accept_more_input_ = true; scoped_refptr last_input_buffer_; + std::deque> written_inputs_; std::vector> decoded_audios_; bool eos_written_ = false; From d2d52e64aa0e87fafc20ca6566447695fbe56fde Mon Sep 17 00:00:00 2001 From: Andrew Savage Date: Wed, 6 Sep 2023 09:16:42 -0700 Subject: [PATCH 002/140] Update LTS minor version to 10 (#1480) First stable version for 24 LTS b/260110906 --- cobalt/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/version.h b/cobalt/version.h index 942ed6b72482..bd8a169a99b0 100644 --- a/cobalt/version.h +++ b/cobalt/version.h @@ -35,6 +35,6 @@ // release is cut. //. -#define COBALT_VERSION "24.lts.6" +#define COBALT_VERSION "24.lts.10" #endif // COBALT_VERSION_H_ From 2047fb590fe0fa68f1dd473278b241eac8f3bed1 Mon Sep 17 00:00:00 2001 From: Andrew Savage Date: Tue, 19 Sep 2023 09:47:35 -0700 Subject: [PATCH 003/140] Update LTS minor version to 11 (#1578) b/260110906 --- cobalt/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/version.h b/cobalt/version.h index bd8a169a99b0..284be91a424e 100644 --- a/cobalt/version.h +++ b/cobalt/version.h @@ -35,6 +35,6 @@ // release is cut. //. -#define COBALT_VERSION "24.lts.10" +#define COBALT_VERSION "24.lts.11" #endif // COBALT_VERSION_H_ From fced32c14ff370c4ae823256bec096d5b3291438 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:15:44 -0700 Subject: [PATCH 004/140] Cherry pick PR #1482: Try to fix the RemoteServiceException (#1491) Co-authored-by: Colin Liang --- .../dev/cobalt/coat/MediaPlaybackService.java | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java index e24faf669874..8bfb464b1d86 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java @@ -22,7 +22,6 @@ import android.app.Service; import android.content.Context; import android.content.Intent; -import android.os.Build; import android.os.Build.VERSION; import android.os.IBinder; import android.os.RemoteException; @@ -37,17 +36,19 @@ public class MediaPlaybackService extends Service { private static final int NOTIFICATION_ID = 193266736; // CL number for uniqueness. private static final String NOTIFICATION_CHANNEL_ID = "dev.cobalt.coat media playback service"; private static final String NOTIFICATION_CHANNEL_NAME = "Media playback service"; + private boolean channelCreated = true; + private NotificationManager notificationManager = null; @Override public void onCreate() { - if (getStarboardBridge() == null) { - Log.e(TAG, "StarboardBridge already destroyed."); - return; - } Log.i(TAG, "Creating a Media playback foreground service."); super.onCreate(); - getStarboardBridge().onServiceStart(this); - createNotificationChannel(); + if (getStarboardBridge() != null) { + getStarboardBridge().onServiceStart(this); + } + this.notificationManager = + (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); + this.channelCreated = createNotificationChannel(); } @Override @@ -66,64 +67,74 @@ public IBinder onBind(Intent intent) { @Override public void onDestroy() { - if (getStarboardBridge() == null) { - Log.e(TAG, "StarboardBridge already destroyed."); - return; - } Log.i(TAG, "Destroying the Media playback service."); - getStarboardBridge().onServiceDestroy(this); + + if (VERSION.SDK_INT >= 26 && this.channelCreated) { + this.notificationManager.deleteNotificationChannel(NOTIFICATION_CHANNEL_ID); + } + + if (getStarboardBridge() != null) { + getStarboardBridge().onServiceDestroy(this); + } super.onDestroy(); } public void startService() { - try { - startForeground(NOTIFICATION_ID, buildNotification()); - } catch (IllegalStateException e) { - Log.e(TAG, "Failed to start Foreground Service", e); + if (this.channelCreated) { + try { + startForeground(NOTIFICATION_ID, buildNotification()); + } catch (IllegalStateException e) { + Log.e(TAG, "Failed to start Foreground Service", e); + } } } public void stopService() { // Let service itself handle notification deletion. - stopForeground(true); + if (this.channelCreated) { + stopForeground(true); + } stopSelf(); } - private void createNotificationChannel() { - if (Build.VERSION.SDK_INT >= 26) { + private boolean createNotificationChannel() { + if (VERSION.SDK_INT >= 26) { try { createNotificationChannelInternalV26(); } catch (RemoteException e) { Log.e(TAG, "Failed to create Notification Channel.", e); + return false; } } + return true; } @RequiresApi(26) private void createNotificationChannelInternalV26() throws RemoteException { - NotificationManager notificationManager = - (NotificationManager) this.getSystemService(Context.NOTIFICATION_SERVICE); NotificationChannel channel = new NotificationChannel( NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_NAME, notificationManager.IMPORTANCE_DEFAULT); channel.setDescription("Channel for showing persistent notification"); - notificationManager.createNotificationChannel(channel); + this.notificationManager.createNotificationChannel(channel); } Notification buildNotification() { + String channelId = ""; + if (VERSION.SDK_INT >= 26) { + // Channel with ID=NOTIFICATION_CHANNEL_ID is created for version >= 26 + channelId = NOTIFICATION_CHANNEL_ID; + } + NotificationCompat.Builder builder = - new NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID) + new NotificationCompat.Builder(this, channelId) .setShowWhen(false) .setPriority(NotificationCompat.PRIORITY_MIN) .setSmallIcon(android.R.drawable.stat_sys_warning) .setContentTitle("Media playback service") .setContentText("Media playback service is running"); - if (VERSION.SDK_INT >= 26) { - builder.setChannelId(NOTIFICATION_CHANNEL_ID); - } return builder.build(); } From 3fe4be656cb62ac494fd3169581b09c96a9c95a9 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:32:01 -0700 Subject: [PATCH 005/140] Cherry pick PR #1088: Update media info after play/pause and rate change (#1484) Refer to the original PR: https://github.com/youtube/cobalt/pull/1088 Update media info immediate after play/pause and playback rate change to make reported media time more smooth. b/293925319 Co-authored-by: Jason --- .../player/filter/filter_based_player_worker_handler.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc index 4a1713ef105c..56e60fae3813 100644 --- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc +++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc @@ -351,7 +351,7 @@ bool FilterBasedPlayerWorkerHandler::SetPause(bool pause) { } else { media_time_provider_->Play(); } - + Update(); return true; } @@ -369,6 +369,7 @@ bool FilterBasedPlayerWorkerHandler::SetPlaybackRate(double playback_rate) { } media_time_provider_->SetPlaybackRate(playback_rate_); + Update(); return true; } @@ -500,6 +501,7 @@ void FilterBasedPlayerWorkerHandler::Update() { update_media_info_cb_(media_time, dropped_frames, !is_underflow); } + RemoveJobByToken(update_job_token_); update_job_token_ = Schedule(update_job_, kUpdateInterval); } From 5160f48c25bcf104e52ff152ba296c24f706b914 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:32:17 -0700 Subject: [PATCH 006/140] Cherry pick PR #1538: Avoid reading audio configurations for url player (#1546) Refer to the original PR: https://github.com/youtube/cobalt/pull/1538 b/230897553 Co-authored-by: Jason --- cobalt/media/base/sbplayer_pipeline.cc | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc index ee6a1dc174a0..c79ead5950c2 100644 --- a/cobalt/media/base/sbplayer_pipeline.cc +++ b/cobalt/media/base/sbplayer_pipeline.cc @@ -563,6 +563,13 @@ std::vector SbPlayerPipeline::GetAudioConnectors() const { std::vector connectors; +#if SB_HAS(PLAYER_WITH_URL) + // Url based player does not support audio connectors. + if (is_url_based_) { + return connectors; + } +#endif // SB_HAS(PLAYER_WITH_URL) + auto configurations = player_bridge_->GetAudioConfigurations(); for (auto&& configuration : configurations) { connectors.push_back(GetMediaAudioConnectorName(configuration.connector)); @@ -1157,6 +1164,14 @@ void SbPlayerPipeline::OnPlayerStatus(SbPlayerState state) { playback_statistics_.OnPresenting( video_stream_->video_decoder_config()); } + +#if SB_HAS(PLAYER_WITH_URL) + // Url based player does not support |audio_write_duration_for_preroll_|. + if (is_url_based_) { + break; + } +#endif // SB_HAS(PLAYER_WITH_URL) + #if SB_API_VERSION >= 15 audio_write_duration_for_preroll_ = audio_write_duration_ = HasRemoteAudioOutputs(player_bridge_->GetAudioConfigurations()) From 6b0d8eb3df15cdb9a0ccbdbfdae3e716aede69f9 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:42:06 -0700 Subject: [PATCH 007/140] Cherry pick PR #1461: [android] Allow to enable tunnel mode via mime attributes (#1470) Refer to the original PR: https://github.com/youtube/cobalt/pull/1461 So it can be enabled in the webapp for experimental purposes. Also added extra logs regarding the enabling of tunnel mode. b/182926876 Co-authored-by: xiaomings --- .../shared/player_components_factory.h | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/starboard/android/shared/player_components_factory.h b/starboard/android/shared/player_components_factory.h index 18db0ba601a6..1530d5e96662 100644 --- a/starboard/android/shared/player_components_factory.h +++ b/starboard/android/shared/player_components_factory.h @@ -331,20 +331,19 @@ class PlayerComponentsFactory : public starboard::shared::starboard::player:: bool enable_tunnel_mode = false; if (creation_parameters.audio_codec() != kSbMediaAudioCodecNone && creation_parameters.video_codec() != kSbMediaVideoCodecNone) { - bool enable_tunnel_mode = + enable_tunnel_mode = audio_mime_type.GetParamBoolValue("tunnelmode", false) && video_mime_type.GetParamBoolValue("tunnelmode", false); - if (!enable_tunnel_mode) { - SB_LOG(INFO) << "Tunnel mode is disabled. " - << "Audio mime parameter \"tunnelmode\" value: " - << audio_mime_type.GetParamStringValue("tunnelmode", - "") - << ", video mime parameter \"tunnelmode\" value: " - << video_mime_type.GetParamStringValue("tunnelmode", - "") - << "."; - } + SB_LOG(INFO) << "Tunnel mode is " + << (enable_tunnel_mode ? "enabled. " : "disabled. ") + << "Audio mime parameter \"tunnelmode\" value: " + << audio_mime_type.GetParamStringValue("tunnelmode", + "") + << ", video mime parameter \"tunnelmode\" value: " + << video_mime_type.GetParamStringValue("tunnelmode", + "") + << "."; } else { SB_LOG(INFO) << "Tunnel mode requires both an audio and video stream. " << "Audio codec: " @@ -365,12 +364,19 @@ class PlayerComponentsFactory : public starboard::shared::starboard::player:: SB_LOG_IF(INFO, !force_improved_support_check) << "Improved support check is disabled for queries under 4K."; bool force_secure_pipeline_under_tunnel_mode = false; - if (enable_tunnel_mode && - IsTunnelModeSupported(creation_parameters, - &force_secure_pipeline_under_tunnel_mode, - force_improved_support_check)) { - tunnel_mode_audio_session_id = GenerateAudioSessionId( - creation_parameters, force_improved_support_check); + if (enable_tunnel_mode) { + if (IsTunnelModeSupported(creation_parameters, + &force_secure_pipeline_under_tunnel_mode, + force_improved_support_check)) { + tunnel_mode_audio_session_id = GenerateAudioSessionId( + creation_parameters, force_improved_support_check); + SB_LOG(INFO) << "Generated tunnel mode audio session id " + << tunnel_mode_audio_session_id; + } else { + SB_LOG(INFO) << "IsTunnelModeSupported() failed, disable tunnel mode."; + } + } else { + SB_LOG(INFO) << "Tunnel mode not enabled."; } if (tunnel_mode_audio_session_id == -1) { From 32e74ae85b4375b50900ed2c63e3da9b3e06f94c Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:43:20 -0700 Subject: [PATCH 008/140] Cherry pick PR #1411: [android] Refactor MediaCodecUtil.dumpAllDecoders() (#1421) Refer to the original PR: https://github.com/youtube/cobalt/pull/1411 Move it to its own class MediaCodecCapabilitiesLogger. b/171144651 Co-authored-by: xiaomings --- starboard/android/apk/apk_sources.gni | 1 + .../java/dev/cobalt/coat/CobaltActivity.java | 4 +- .../media/MediaCodecCapabilitiesLogger.java | 216 ++++++++++++++++++ .../java/dev/cobalt/media/MediaCodecUtil.java | 204 ++--------------- 4 files changed, 233 insertions(+), 192 deletions(-) create mode 100644 starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java diff --git a/starboard/android/apk/apk_sources.gni b/starboard/android/apk/apk_sources.gni index d1f668cdc1f3..456420209465 100644 --- a/starboard/android/apk/apk_sources.gni +++ b/starboard/android/apk/apk_sources.gni @@ -47,6 +47,7 @@ apk_sources = [ "//starboard/android/apk/app/src/main/java/dev/cobalt/media/CobaltMediaSession.java", "//starboard/android/apk/app/src/main/java/dev/cobalt/media/Log.java", "//starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java", + "//starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java", "//starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java", "//starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java", "//starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaImage.java", diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java index d94210252b07..2a763280dd1d 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java @@ -32,7 +32,7 @@ import androidx.annotation.CallSuper; import com.google.androidgamesdk.GameActivity; import dev.cobalt.media.AudioOutputManager; -import dev.cobalt.media.MediaCodecUtil; +import dev.cobalt.media.MediaCodecCapabilitiesLogger; import dev.cobalt.media.VideoSurfaceView; import dev.cobalt.util.DisplayUtil; import dev.cobalt.util.Log; @@ -146,7 +146,7 @@ protected StarboardBridge getStarboardBridge() { protected void onStart() { if (!isReleaseBuild()) { getStarboardBridge().getAudioOutputManager().dumpAllOutputDevices(); - MediaCodecUtil.dumpAllDecoders(); + MediaCodecCapabilitiesLogger.dumpAllDecoders(); } if (forceCreateNewVideoSurfaceView) { Log.w(TAG, "Force to create a new video surface."); diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java new file mode 100644 index 000000000000..27211118df9c --- /dev/null +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java @@ -0,0 +1,216 @@ +// Copyright 2017 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. + +package dev.cobalt.media; + +import static dev.cobalt.media.Log.TAG; + +import android.media.MediaCodecInfo; +import android.media.MediaCodecInfo.CodecCapabilities; +import android.media.MediaCodecInfo.VideoCapabilities; +import android.media.MediaCodecList; +import dev.cobalt.util.Log; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Locale; + +/** Utility class to log MediaCodec capabilities. */ +public class MediaCodecCapabilitiesLogger { + /** Utility class to save the maximum supported resolution and frame rate of a decoder. */ + static class ResolutionAndFrameRate { + public ResolutionAndFrameRate(Integer width, Integer height, Double frameRate) { + this.width = width; + this.height = height; + this.frameRate = frameRate; + } + + public boolean isCovered(Integer width, Integer height, Double frameRate) { + return this.width >= width && this.height >= height && this.frameRate >= frameRate; + } + + public Integer width = -1; + public Integer height = -1; + public Double frameRate = -1.0; + } + + /** Returns a string detailing SDR and HDR capabilities of a decoder. */ + public static String getSupportedResolutionsAndFrameRates( + VideoCapabilities videoCapabilities, boolean isHdrCapable) { + ArrayList> resolutionList = + new ArrayList<>( + Arrays.asList( + new ArrayList<>(Arrays.asList(7680, 4320)), + new ArrayList<>(Arrays.asList(3840, 2160)), + new ArrayList<>(Arrays.asList(2560, 1440)), + new ArrayList<>(Arrays.asList(1920, 1080)), + new ArrayList<>(Arrays.asList(1280, 720)))); + ArrayList frameRateList = + new ArrayList<>(Arrays.asList(60.0, 59.997, 50.0, 48.0, 30.0, 29.997, 25.0, 24.0, 23.997)); + ArrayList supportedResolutionsAndFrameRates = new ArrayList<>(); + for (Double frameRate : frameRateList) { + for (ArrayList resolution : resolutionList) { + boolean isResolutionAndFrameRateCovered = false; + for (ResolutionAndFrameRate resolutionAndFrameRate : supportedResolutionsAndFrameRates) { + if (resolutionAndFrameRate.isCovered(resolution.get(0), resolution.get(1), frameRate)) { + isResolutionAndFrameRateCovered = true; + break; + } + } + if (videoCapabilities.areSizeAndRateSupported( + resolution.get(0), resolution.get(1), frameRate)) { + if (!isResolutionAndFrameRateCovered) { + supportedResolutionsAndFrameRates.add( + new ResolutionAndFrameRate(resolution.get(0), resolution.get(1), frameRate)); + } + continue; + } + if (isResolutionAndFrameRateCovered) { + // This configuration should be covered by a supported configuration, return long form. + return getLongFormSupportedResolutionsAndFrameRates( + resolutionList, frameRateList, videoCapabilities, isHdrCapable); + } + } + } + return convertResolutionAndFrameRatesToString(supportedResolutionsAndFrameRates, isHdrCapable); + } + + /** + * Like getSupportedResolutionsAndFrameRates(), but returns the full information for each frame + * rate and resolution combination. + */ + public static String getLongFormSupportedResolutionsAndFrameRates( + ArrayList> resolutionList, + ArrayList frameRateList, + VideoCapabilities videoCapabilities, + boolean isHdrCapable) { + ArrayList supported = new ArrayList<>(); + for (Double frameRate : frameRateList) { + for (ArrayList resolution : resolutionList) { + if (videoCapabilities.areSizeAndRateSupported( + resolution.get(0), resolution.get(1), frameRate)) { + supported.add( + new ResolutionAndFrameRate(resolution.get(0), resolution.get(1), frameRate)); + } + } + } + return convertResolutionAndFrameRatesToString(supported, isHdrCapable); + } + + /** Convert a list of ResolutionAndFrameRate to a human readable string. */ + public static String convertResolutionAndFrameRatesToString( + ArrayList supported, boolean isHdrCapable) { + if (supported.isEmpty()) { + return "None. "; + } + String frameRateAndResolutionString = ""; + for (ResolutionAndFrameRate resolutionAndFrameRate : supported) { + frameRateAndResolutionString += + String.format( + Locale.US, + "[%d x %d, %.3f fps], ", + resolutionAndFrameRate.width, + resolutionAndFrameRate.height, + resolutionAndFrameRate.frameRate); + } + frameRateAndResolutionString += isHdrCapable ? "hdr/sdr, " : "sdr, "; + return frameRateAndResolutionString; + } + + /** + * Debug utility function that can be locally added to dump information about all decoders on a + * particular system. + */ + public static void dumpAllDecoders() { + String decoderDumpString = ""; + for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { + if (info.isEncoder()) { + continue; + } + for (String supportedType : info.getSupportedTypes()) { + String name = info.getName(); + decoderDumpString += + String.format( + Locale.US, + "name: %s (%s, %s): ", + name, + supportedType, + MediaCodecUtil.isCodecDenyListed(name) ? "denylisted" : "not denylisted"); + CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType); + VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities(); + String resultName = + (codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback) + && !name.endsWith(MediaCodecUtil.getSecureDecoderSuffix())) + ? (name + MediaCodecUtil.getSecureDecoderSuffix()) + : name; + boolean isHdrCapable = + MediaCodecUtil.isHdrCapableVideoDecoder( + codecCapabilities.getMimeType(), codecCapabilities); + if (videoCapabilities != null) { + String frameRateAndResolutionString = + getSupportedResolutionsAndFrameRates(videoCapabilities, isHdrCapable); + decoderDumpString += + String.format( + Locale.US, + "\n\t\t" + + "widths: %s, " + + "heights: %s, " + + "bitrates: %s, " + + "framerates: %s, " + + "supported sizes and framerates: %s", + videoCapabilities.getSupportedWidths().toString(), + videoCapabilities.getSupportedHeights().toString(), + videoCapabilities.getBitrateRange().toString(), + videoCapabilities.getSupportedFrameRates().toString(), + frameRateAndResolutionString); + } + boolean isAdaptivePlaybackSupported = + codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback); + boolean isSecurePlaybackSupported = + codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback); + boolean isTunneledPlaybackSupported = + codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback); + if (isAdaptivePlaybackSupported + || isSecurePlaybackSupported + || isTunneledPlaybackSupported) { + decoderDumpString += + String.format( + Locale.US, + "(%s%s%s", + isAdaptivePlaybackSupported ? "AdaptivePlayback, " : "", + isSecurePlaybackSupported ? "SecurePlayback, " : "", + isTunneledPlaybackSupported ? "TunneledPlayback, " : ""); + // Remove trailing space and comma + decoderDumpString = decoderDumpString.substring(0, decoderDumpString.length() - 2); + decoderDumpString += ")"; + } else { + decoderDumpString += " No extra features supported"; + } + decoderDumpString += "\n"; + } + } + Log.v( + TAG, + " \n" + + "==================================================\n" + + "Full list of decoder features: [AdaptivePlayback, SecurePlayback," + + " TunneledPlayback]\n" + + "Unsupported features for each codec are not listed\n" + + decoderDumpString + + "=================================================="); + } +} diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java index 9afa2bc1afb9..ebe9c2bd0e50 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java @@ -28,7 +28,6 @@ import dev.cobalt.util.Log; import dev.cobalt.util.UsedByNative; import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.List; @@ -442,7 +441,7 @@ public static CodecCapabilityInfo[] getAllCodecCapabilityInfos() { // Filter blacklisted video decoders. String name = codecInfo.getName(); - if (!isVp9AllowListed && videoCodecDenyList.contains(name)) { + if (!isVp9AllowListed && isCodecDenyListed(name)) { Log.v(TAG, "Rejecting %s, reason: codec is on deny list", name); continue; } @@ -451,7 +450,7 @@ public static CodecCapabilityInfo[] getAllCodecCapabilityInfos() { // denylisted. String nameWithoutSecureSuffix = name.substring(0, name.length() - SECURE_DECODER_SUFFIX.length()); - if (!isVp9AllowListed && videoCodecDenyList.contains(nameWithoutSecureSuffix)) { + if (!isVp9AllowListed && isCodecDenyListed(nameWithoutSecureSuffix)) { String format = "Rejecting %s, reason: offpsec denylisted secure decoder"; Log.v(TAG, format, name); continue; @@ -466,6 +465,16 @@ public static CodecCapabilityInfo[] getAllCodecCapabilityInfos() { return codecCapabilityInfos.toArray(array); } + /** Returns whether the codec is denylisted. */ + public static boolean isCodecDenyListed(String codecName) { + return videoCodecDenyList.contains(codecName); + } + + /** Simply returns SECURE_DECODER_SUFFIX to allow access to it. */ + public static String getSecureDecoderSuffix() { + return SECURE_DECODER_SUFFIX; + } + /** Determine whether codecCapabilities is capable of playing HDR. */ public static boolean isHdrCapableVideoDecoder( String mimeType, CodecCapabilities codecCapabilities) { @@ -570,7 +579,7 @@ public static String findVideoDecoder( for (VideoDecoderCache.CachedDecoder decoder : VideoDecoderCache.getCachedDecoders(mimeType, decoderCacheTtlMs)) { String name = decoder.info.getName(); - if (!isVp9AllowListed && videoCodecDenyList.contains(name)) { + if (!isVp9AllowListed && isCodecDenyListed(name)) { Log.v(TAG, "Rejecting " + name + ", reason: codec is on deny list"); continue; } @@ -596,7 +605,7 @@ public static String findVideoDecoder( // denylisted. String nameWithoutSecureSuffix = name.substring(0, name.length() - SECURE_DECODER_SUFFIX.length()); - if (!isVp9AllowListed && videoCodecDenyList.contains(nameWithoutSecureSuffix)) { + if (!isVp9AllowListed && isCodecDenyListed(nameWithoutSecureSuffix)) { Log.v(TAG, "Rejecting " + name + ", reason: denylisted secure decoder"); } } @@ -770,189 +779,4 @@ public static String findAudioDecoder( } return ""; } - - /** Utility class to save the maximum supported resolution and frame rate of a decoder. */ - static class ResolutionAndFrameRate { - public ResolutionAndFrameRate(Integer width, Integer height, Double frameRate) { - this.width = width; - this.height = height; - this.frameRate = frameRate; - } - - public boolean isCovered(Integer width, Integer height, Double frameRate) { - return this.width >= width && this.height >= height && this.frameRate >= frameRate; - } - - public Integer width = -1; - public Integer height = -1; - public Double frameRate = -1.0; - } - - /** Returns a string detailing SDR and HDR capabilities of a decoder. */ - public static String getSupportedResolutionsAndFrameRates( - VideoCapabilities videoCapabilities, boolean isHdrCapable) { - ArrayList> resolutionList = - new ArrayList<>( - Arrays.asList( - new ArrayList<>(Arrays.asList(7680, 4320)), - new ArrayList<>(Arrays.asList(3840, 2160)), - new ArrayList<>(Arrays.asList(2560, 1440)), - new ArrayList<>(Arrays.asList(1920, 1080)), - new ArrayList<>(Arrays.asList(1280, 720)))); - ArrayList frameRateList = - new ArrayList<>(Arrays.asList(60.0, 59.997, 50.0, 48.0, 30.0, 29.997, 25.0, 24.0, 23.997)); - ArrayList supportedResolutionsAndFrameRates = new ArrayList<>(); - for (Double frameRate : frameRateList) { - for (ArrayList resolution : resolutionList) { - boolean isResolutionAndFrameRateCovered = false; - for (ResolutionAndFrameRate resolutionAndFrameRate : supportedResolutionsAndFrameRates) { - if (resolutionAndFrameRate.isCovered(resolution.get(0), resolution.get(1), frameRate)) { - isResolutionAndFrameRateCovered = true; - break; - } - } - if (videoCapabilities.areSizeAndRateSupported( - resolution.get(0), resolution.get(1), frameRate)) { - if (!isResolutionAndFrameRateCovered) { - supportedResolutionsAndFrameRates.add( - new ResolutionAndFrameRate(resolution.get(0), resolution.get(1), frameRate)); - } - continue; - } - if (isResolutionAndFrameRateCovered) { - // This configuration should be covered by a supported configuration, return long form. - return getLongFormSupportedResolutionsAndFrameRates( - resolutionList, frameRateList, videoCapabilities, isHdrCapable); - } - } - } - return convertResolutionAndFrameRatesToString(supportedResolutionsAndFrameRates, isHdrCapable); - } - - /** - * Like getSupportedResolutionsAndFrameRates(), but returns the full information for each frame - * rate and resolution combination. - */ - public static String getLongFormSupportedResolutionsAndFrameRates( - ArrayList> resolutionList, - ArrayList frameRateList, - VideoCapabilities videoCapabilities, - boolean isHdrCapable) { - ArrayList supported = new ArrayList<>(); - for (Double frameRate : frameRateList) { - for (ArrayList resolution : resolutionList) { - if (videoCapabilities.areSizeAndRateSupported( - resolution.get(0), resolution.get(1), frameRate)) { - supported.add( - new ResolutionAndFrameRate(resolution.get(0), resolution.get(1), frameRate)); - } - } - } - return convertResolutionAndFrameRatesToString(supported, isHdrCapable); - } - - public static String convertResolutionAndFrameRatesToString( - ArrayList supported, boolean isHdrCapable) { - if (supported.isEmpty()) { - return "None. "; - } - String frameRateAndResolutionString = ""; - for (ResolutionAndFrameRate resolutionAndFrameRate : supported) { - frameRateAndResolutionString += - String.format( - Locale.US, - "[%d x %d, %.3f fps], ", - resolutionAndFrameRate.width, - resolutionAndFrameRate.height, - resolutionAndFrameRate.frameRate); - } - frameRateAndResolutionString += isHdrCapable ? "hdr/sdr, " : "sdr, "; - return frameRateAndResolutionString; - } - - /** - * Debug utility function that can be locally added to dump information about all decoders on a - * particular system. - */ - public static void dumpAllDecoders() { - String decoderDumpString = ""; - for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { - if (info.isEncoder()) { - continue; - } - for (String supportedType : info.getSupportedTypes()) { - String name = info.getName(); - decoderDumpString += - String.format( - Locale.US, - "name: %s (%s, %s): ", - name, - supportedType, - videoCodecDenyList.contains(name) ? "denylisted" : "not denylisted"); - CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType); - VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities(); - String resultName = - (codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback) - && !name.endsWith(SECURE_DECODER_SUFFIX)) - ? (name + SECURE_DECODER_SUFFIX) - : name; - boolean isHdrCapable = - isHdrCapableVideoDecoder(codecCapabilities.getMimeType(), codecCapabilities); - if (videoCapabilities != null) { - String frameRateAndResolutionString = - getSupportedResolutionsAndFrameRates(videoCapabilities, isHdrCapable); - decoderDumpString += - String.format( - Locale.US, - "\n\t\t" - + "widths: %s, " - + "heights: %s, " - + "bitrates: %s, " - + "framerates: %s, " - + "supported sizes and framerates: %s", - videoCapabilities.getSupportedWidths().toString(), - videoCapabilities.getSupportedHeights().toString(), - videoCapabilities.getBitrateRange().toString(), - videoCapabilities.getSupportedFrameRates().toString(), - frameRateAndResolutionString); - } - boolean isAdaptivePlaybackSupported = - codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback); - boolean isSecurePlaybackSupported = - codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback); - boolean isTunneledPlaybackSupported = - codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback); - if (isAdaptivePlaybackSupported - || isSecurePlaybackSupported - || isTunneledPlaybackSupported) { - decoderDumpString += - String.format( - Locale.US, - "(%s%s%s", - isAdaptivePlaybackSupported ? "AdaptivePlayback, " : "", - isSecurePlaybackSupported ? "SecurePlayback, " : "", - isTunneledPlaybackSupported ? "TunneledPlayback, " : ""); - // Remove trailing space and comma - decoderDumpString = decoderDumpString.substring(0, decoderDumpString.length() - 2); - decoderDumpString += ")"; - } else { - decoderDumpString += " No extra features supported"; - } - decoderDumpString += "\n"; - } - } - Log.v( - TAG, - " \n" - + "==================================================\n" - + "Full list of decoder features: [AdaptivePlayback, SecurePlayback," - + " TunneledPlayback]\n" - + "Unsupported features for each codec are not listed\n" - + decoderDumpString - + "=================================================="); - } } From 5a903005fb715d23f728af10efd1a1200a56da02 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:54:45 -0700 Subject: [PATCH 009/140] Cherry pick PR #1455: [android] Cache audio configurations (#1580) Refer to the original PR: https://github.com/youtube/cobalt/pull/1455 1. Cache audio configurations and always get max supported channels from the configurations. 2. Remove getMaxChannels() and its related code. 3. Clear MediaCapabilitiesCache after audio device change as it could change passthrough decoder capabilities. b/284140486 Co-authored-by: Jason --- .../dev/cobalt/media/AudioOutputManager.java | 44 ++--- .../shared/media_capabilities_cache.cc | 184 ++++++++++++++++-- .../android/shared/media_capabilities_cache.h | 8 +- .../shared/media_get_audio_configuration.cc | 155 +-------------- starboard/shared/starboard/media/mime_util.cc | 20 +- 5 files changed, 197 insertions(+), 214 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java index 9fb274f87d4b..0e02ebc7851a 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/AudioOutputManager.java @@ -142,39 +142,6 @@ void destroyAudioTrackBridge(AudioTrackBridge audioTrackBridge) { audioTrackBridgeList.remove(audioTrackBridge); } - /** Returns the maximum number of HDMI channels. */ - @SuppressWarnings("unused") - @UsedByNative - int getMaxChannels() { - // The aac audio decoder on this platform will switch its output from 5.1 - // to stereo right before providing the first output buffer when - // attempting to decode 5.1 input. Since this heavily violates invariants - // of the shared starboard player framework, disable 5.1 on this platform. - // It is expected that we will be able to resolve this issue with Xiaomi - // by Android P, so only do this workaround for SDK versions < 27. - if (android.os.Build.MODEL.equals("MIBOX3") && android.os.Build.VERSION.SDK_INT < 27) { - return 2; - } - - AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - AudioDeviceInfo[] deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS); - int maxChannels = 2; - for (AudioDeviceInfo info : deviceInfos) { - int type = info.getType(); - if (type == AudioDeviceInfo.TYPE_HDMI || type == AudioDeviceInfo.TYPE_HDMI_ARC) { - int[] channelCounts = info.getChannelCounts(); - if (channelCounts.length == 0) { - // An empty array indicates that the device supports arbitrary channel masks. - return 8; - } - for (int count : channelCounts) { - maxChannels = Math.max(maxChannels, count); - } - } - } - return maxChannels; - } - /** Stores info from AudioDeviceInfo to be passed to the native app. */ @SuppressWarnings("unused") @UsedByNative @@ -208,8 +175,19 @@ boolean getOutputDeviceInfo(int index, OutputDeviceInfo outDeviceInfo) { outDeviceInfo.type = deviceInfos[index].getType(); outDeviceInfo.channels = 2; + // The aac audio decoder on this platform will switch its output from 5.1 + // to stereo right before providing the first output buffer when + // attempting to decode 5.1 input. Since this heavily violates invariants + // of the shared starboard player framework, disable 5.1 on this platform. + // It is expected that we will be able to resolve this issue with Xiaomi + // by Android P, so only do this workaround for SDK versions < 27. + if (android.os.Build.MODEL.equals("MIBOX3") && android.os.Build.VERSION.SDK_INT < 27) { + return true; + } + int[] channelCounts = deviceInfos[index].getChannelCounts(); if (channelCounts.length == 0) { + // An empty array indicates that the device supports arbitrary channel masks. outDeviceInfo.channels = 8; } else { for (int count : channelCounts) { diff --git a/starboard/android/shared/media_capabilities_cache.cc b/starboard/android/shared/media_capabilities_cache.cc index 7b8997b3957f..0d508243713a 100644 --- a/starboard/android/shared/media_capabilities_cache.cc +++ b/starboard/android/shared/media_capabilities_cache.cc @@ -40,6 +40,112 @@ const jint HDR_TYPE_HDR10_PLUS = 4; const char SECURE_DECODER_SUFFIX[] = ".secure"; +// Constants for output types from +// https://developer.android.com/reference/android/media/AudioDeviceInfo. +constexpr int TYPE_AUX_LINE = 19; +constexpr int TYPE_BLE_BROADCAST = 30; +constexpr int TYPE_BLE_HEADSET = 26; +constexpr int TYPE_BLE_SPEAKER = 27; +constexpr int TYPE_BLUETOOTH_A2DP = 8; +constexpr int TYPE_BLUETOOTH_SCO = 7; +constexpr int TYPE_BUILTIN_EARPIECE = 1; +constexpr int TYPE_BUILTIN_MIC = 15; +constexpr int TYPE_BUILTIN_SPEAKER = 2; +constexpr int TYPE_BUILTIN_SPEAKER_SAFE = 24; +constexpr int TYPE_BUS = 21; +constexpr int TYPE_DOCK = 13; +constexpr int TYPE_DOCK_ANALOG = 31; +constexpr int TYPE_FM = 14; +constexpr int TYPE_FM_TUNER = 16; +constexpr int TYPE_HDMI = 9; +constexpr int TYPE_HDMI_ARC = 10; +constexpr int TYPE_HDMI_EARC = 29; +constexpr int TYPE_HEARING_AID = 23; +constexpr int TYPE_IP = 20; +constexpr int TYPE_LINE_ANALOG = 5; +constexpr int TYPE_LINE_DIGITAL = 6; +constexpr int TYPE_REMOTE_SUBMIX = 25; +constexpr int TYPE_TELEPHONY = 18; +constexpr int TYPE_TV_TUNER = 17; +constexpr int TYPE_UNKNOWN = 0; +constexpr int TYPE_USB_ACCESSORY = 12; +constexpr int TYPE_USB_DEVICE = 11; +constexpr int TYPE_USB_HEADSET = 22; +constexpr int TYPE_WIRED_HEADPHONES = 4; +constexpr int TYPE_WIRED_HEADSET = 3; + +SbMediaAudioConnector GetConnectorFromAndroidOutputType( + int android_output_device_type) { + switch (android_output_device_type) { + case TYPE_AUX_LINE: + return kSbMediaAudioConnectorAnalog; + case TYPE_BLE_BROADCAST: + return kSbMediaAudioConnectorBluetooth; + case TYPE_BLE_HEADSET: + return kSbMediaAudioConnectorBluetooth; + case TYPE_BLE_SPEAKER: + return kSbMediaAudioConnectorBluetooth; + case TYPE_BLUETOOTH_A2DP: + return kSbMediaAudioConnectorBluetooth; + case TYPE_BLUETOOTH_SCO: + return kSbMediaAudioConnectorBluetooth; + case TYPE_BUILTIN_EARPIECE: + return kSbMediaAudioConnectorBuiltIn; + case TYPE_BUILTIN_MIC: + return kSbMediaAudioConnectorBuiltIn; + case TYPE_BUILTIN_SPEAKER: + return kSbMediaAudioConnectorBuiltIn; + case TYPE_BUILTIN_SPEAKER_SAFE: + return kSbMediaAudioConnectorBuiltIn; + case TYPE_BUS: + return kSbMediaAudioConnectorUnknown; + case TYPE_DOCK: + return kSbMediaAudioConnectorUnknown; + case TYPE_DOCK_ANALOG: + return kSbMediaAudioConnectorAnalog; + case TYPE_FM: + return kSbMediaAudioConnectorUnknown; + case TYPE_FM_TUNER: + return kSbMediaAudioConnectorUnknown; + case TYPE_HDMI: + return kSbMediaAudioConnectorHdmi; + case TYPE_HDMI_ARC: + return kSbMediaAudioConnectorHdmi; + case TYPE_HDMI_EARC: + return kSbMediaAudioConnectorHdmi; + case TYPE_HEARING_AID: + return kSbMediaAudioConnectorUnknown; + case TYPE_IP: + return kSbMediaAudioConnectorRemoteWired; + case TYPE_LINE_ANALOG: + return kSbMediaAudioConnectorAnalog; + case TYPE_LINE_DIGITAL: + return kSbMediaAudioConnectorUnknown; + case TYPE_REMOTE_SUBMIX: + return kSbMediaAudioConnectorRemoteOther; + case TYPE_TELEPHONY: + return kSbMediaAudioConnectorUnknown; + case TYPE_TV_TUNER: + return kSbMediaAudioConnectorUnknown; + case TYPE_UNKNOWN: + return kSbMediaAudioConnectorUnknown; + case TYPE_USB_ACCESSORY: + return kSbMediaAudioConnectorUsb; + case TYPE_USB_DEVICE: + return kSbMediaAudioConnectorUsb; + case TYPE_USB_HEADSET: + return kSbMediaAudioConnectorUsb; + case TYPE_WIRED_HEADPHONES: + return kSbMediaAudioConnectorAnalog; + case TYPE_WIRED_HEADSET: + return kSbMediaAudioConnectorAnalog; + } + + SB_LOG(WARNING) << "Encountered unknown audio output device type " + << android_output_device_type; + return kSbMediaAudioConnectorUnknown; +} + bool EndsWith(const std::string& str, const std::string& suffix) { if (str.size() < suffix.size()) { return false; @@ -135,13 +241,43 @@ bool GetIsPassthroughSupported(SbMediaAudioCodec codec) { encoding) == JNI_TRUE; } -int GetMaxAudioOutputChannels() { +bool GetAudioConfiguration(int index, + SbMediaAudioConfiguration* configuration) { + *configuration = {}; + JniEnvExt* env = JniEnvExt::Get(); ScopedLocalJavaRef j_audio_output_manager( env->CallStarboardObjectMethodOrAbort( "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;")); - return static_cast(env->CallIntMethodOrAbort( - j_audio_output_manager.Get(), "getMaxChannels", "()I")); + ScopedLocalJavaRef j_output_device_info(env->NewObjectOrAbort( + "dev/cobalt/media/AudioOutputManager$OutputDeviceInfo", "()V")); + + bool succeeded = env->CallBooleanMethodOrAbort( + j_audio_output_manager.Get(), "getOutputDeviceInfo", + "(ILdev/cobalt/media/AudioOutputManager$OutputDeviceInfo;)Z", index, + j_output_device_info.Get()); + + if (!succeeded) { + SB_LOG(WARNING) + << "Call to AudioOutputManager.getOutputDeviceInfo() failed."; + return false; + } + + auto call_int_method = [env, &j_output_device_info](const char* name) { + return env->CallIntMethodOrAbort(j_output_device_info.Get(), name, "()I"); + }; + + configuration->connector = + GetConnectorFromAndroidOutputType(call_int_method("getType")); + configuration->latency = 0; + configuration->coding_type = kSbMediaAudioCodingTypePcm; + configuration->number_of_channels = call_int_method("getChannels"); + + if (configuration->connector != kSbMediaAudioConnectorHdmi) { + configuration->number_of_channels = 2; + } + + return true; } } // namespace @@ -311,14 +447,22 @@ bool MediaCapabilitiesCache::IsPassthroughSupported(SbMediaAudioCodec codec) { return supported; } -int MediaCapabilitiesCache::GetMaxAudioOutputChannels() { +bool MediaCapabilitiesCache::GetAudioConfiguration( + int index, + SbMediaAudioConfiguration* configuration) { + SB_DCHECK(index >= 0); if (!is_enabled_) { - return ::starboard::android::shared::GetMaxAudioOutputChannels(); + return ::starboard::android::shared::GetAudioConfiguration(index, + configuration); } ScopedLock scoped_lock(mutex_); UpdateMediaCapabilities_Locked(); - return max_audio_output_channels_; + if (index < audio_configurations_.size()) { + *configuration = audio_configurations_[index]; + return true; + } + return false; } bool MediaCapabilitiesCache::HasAudioDecoderFor(const std::string& mime_type, @@ -467,14 +611,13 @@ void MediaCapabilitiesCache::ClearCache() { ScopedLock scoped_lock(mutex_); is_initialized_ = false; supported_hdr_types_is_dirty_ = true; - audio_output_channels_is_dirty_ = true; is_widevine_supported_ = false; is_cbcs_supported_ = false; supported_transfer_ids_.clear(); passthrough_supportabilities_.clear(); audio_codec_capabilities_map_.clear(); video_codec_capabilities_map_.clear(); - max_audio_output_channels_ = -1; + audio_configurations_.clear(); } void MediaCapabilitiesCache::Initialize() { @@ -494,10 +637,6 @@ void MediaCapabilitiesCache::UpdateMediaCapabilities_Locked() { if (supported_hdr_types_is_dirty_.exchange(false)) { supported_transfer_ids_ = GetSupportedHdrTypes(); } - if (audio_output_channels_is_dirty_.exchange(false)) { - max_audio_output_channels_ = - ::starboard::android::shared::GetMaxAudioOutputChannels(); - } if (is_initialized_) { return; @@ -505,6 +644,7 @@ void MediaCapabilitiesCache::UpdateMediaCapabilities_Locked() { is_widevine_supported_ = GetIsWidevineSupported(); is_cbcs_supported_ = GetIsCbcsSupported(); LoadCodecInfos_Locked(); + LoadAudioConfigurations_Locked(); is_initialized_ = true; } @@ -558,6 +698,22 @@ void MediaCapabilitiesCache::LoadCodecInfos_Locked() { } } +void MediaCapabilitiesCache::LoadAudioConfigurations_Locked() { + SB_DCHECK(audio_configurations_.empty()); + mutex_.DCheckAcquired(); + + // SbPlayerBridge::GetAudioConfigurations() reads up to 32 configurations. The + // limit here is to avoid infinite loop and also match + // SbPlayerBridge::GetAudioConfigurations(). + const int kMaxAudioConfigurations = 32; + SbMediaAudioConfiguration configuration; + while (audio_configurations_.size() < kMaxAudioConfigurations && + ::starboard::android::shared::GetAudioConfiguration( + audio_configurations_.size(), &configuration)) { + audio_configurations_.push_back(configuration); + } +} + extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_util_DisplayUtil_nativeOnDisplayChanged() { SB_DLOG(INFO) << "Display device has changed."; @@ -568,7 +724,9 @@ Java_dev_cobalt_util_DisplayUtil_nativeOnDisplayChanged() { extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_media_AudioOutputManager_nativeOnAudioDeviceChanged() { SB_DLOG(INFO) << "Audio device has changed."; - MediaCapabilitiesCache::GetInstance()->ClearAudioOutputChannels(); + // Audio output device change could change passthrough decoder capabilities, + // so we have to reload codec capabilities. + MediaCapabilitiesCache::GetInstance()->ClearCache(); MimeSupportabilityCache::GetInstance()->ClearCachedMimeSupportabilities(); } diff --git a/starboard/android/shared/media_capabilities_cache.h b/starboard/android/shared/media_capabilities_cache.h index db3879e0aab5..0395d51683ca 100644 --- a/starboard/android/shared/media_capabilities_cache.h +++ b/starboard/android/shared/media_capabilities_cache.h @@ -125,7 +125,8 @@ class MediaCapabilitiesCache { bool IsPassthroughSupported(SbMediaAudioCodec codec); - int GetMaxAudioOutputChannels(); + bool GetAudioConfiguration(int index, + SbMediaAudioConfiguration* configuration); bool HasAudioDecoderFor(const std::string& mime_type, int bitrate, @@ -162,7 +163,6 @@ class MediaCapabilitiesCache { void Initialize(); void ClearSupportedHdrTypes() { supported_hdr_types_is_dirty_ = true; } - void ClearAudioOutputChannels() { audio_output_channels_is_dirty_ = true; } private: MediaCapabilitiesCache(); @@ -172,6 +172,7 @@ class MediaCapabilitiesCache { MediaCapabilitiesCache& operator=(const MediaCapabilitiesCache&) = delete; void UpdateMediaCapabilities_Locked(); + void LoadAudioConfigurations_Locked(); void LoadCodecInfos_Locked(); Mutex mutex_; @@ -186,14 +187,13 @@ class MediaCapabilitiesCache { std::map audio_codec_capabilities_map_; std::map video_codec_capabilities_map_; + std::vector audio_configurations_; std::atomic_bool is_enabled_{true}; std::atomic_bool supported_hdr_types_is_dirty_{true}; - std::atomic_bool audio_output_channels_is_dirty_{true}; bool is_initialized_ = false; bool is_widevine_supported_ = false; bool is_cbcs_supported_ = false; - int max_audio_output_channels_ = -1; }; } // namespace shared diff --git a/starboard/android/shared/media_get_audio_configuration.cc b/starboard/android/shared/media_get_audio_configuration.cc index 6b645b6e19ac..572fceb0c9e2 100644 --- a/starboard/android/shared/media_get_audio_configuration.cc +++ b/starboard/android/shared/media_get_audio_configuration.cc @@ -19,112 +19,6 @@ #include "starboard/android/shared/media_capabilities_cache.h" #include "starboard/common/media.h" -// Constants for output types from -// https://developer.android.com/reference/android/media/AudioDeviceInfo. -constexpr int TYPE_AUX_LINE = 19; -constexpr int TYPE_BLE_BROADCAST = 30; -constexpr int TYPE_BLE_HEADSET = 26; -constexpr int TYPE_BLE_SPEAKER = 27; -constexpr int TYPE_BLUETOOTH_A2DP = 8; -constexpr int TYPE_BLUETOOTH_SCO = 7; -constexpr int TYPE_BUILTIN_EARPIECE = 1; -constexpr int TYPE_BUILTIN_MIC = 15; -constexpr int TYPE_BUILTIN_SPEAKER = 2; -constexpr int TYPE_BUILTIN_SPEAKER_SAFE = 24; -constexpr int TYPE_BUS = 21; -constexpr int TYPE_DOCK = 13; -constexpr int TYPE_DOCK_ANALOG = 31; -constexpr int TYPE_FM = 14; -constexpr int TYPE_FM_TUNER = 16; -constexpr int TYPE_HDMI = 9; -constexpr int TYPE_HDMI_ARC = 10; -constexpr int TYPE_HDMI_EARC = 29; -constexpr int TYPE_HEARING_AID = 23; -constexpr int TYPE_IP = 20; -constexpr int TYPE_LINE_ANALOG = 5; -constexpr int TYPE_LINE_DIGITAL = 6; -constexpr int TYPE_REMOTE_SUBMIX = 25; -constexpr int TYPE_TELEPHONY = 18; -constexpr int TYPE_TV_TUNER = 17; -constexpr int TYPE_UNKNOWN = 0; -constexpr int TYPE_USB_ACCESSORY = 12; -constexpr int TYPE_USB_DEVICE = 11; -constexpr int TYPE_USB_HEADSET = 22; -constexpr int TYPE_WIRED_HEADPHONES = 4; -constexpr int TYPE_WIRED_HEADSET = 3; - -SbMediaAudioConnector GetConnectorFromAndroidOutputType( - int android_output_device_type) { - switch (android_output_device_type) { - case TYPE_AUX_LINE: - return kSbMediaAudioConnectorAnalog; - case TYPE_BLE_BROADCAST: - return kSbMediaAudioConnectorBluetooth; - case TYPE_BLE_HEADSET: - return kSbMediaAudioConnectorBluetooth; - case TYPE_BLE_SPEAKER: - return kSbMediaAudioConnectorBluetooth; - case TYPE_BLUETOOTH_A2DP: - return kSbMediaAudioConnectorBluetooth; - case TYPE_BLUETOOTH_SCO: - return kSbMediaAudioConnectorBluetooth; - case TYPE_BUILTIN_EARPIECE: - return kSbMediaAudioConnectorBuiltIn; - case TYPE_BUILTIN_MIC: - return kSbMediaAudioConnectorBuiltIn; - case TYPE_BUILTIN_SPEAKER: - return kSbMediaAudioConnectorBuiltIn; - case TYPE_BUILTIN_SPEAKER_SAFE: - return kSbMediaAudioConnectorBuiltIn; - case TYPE_BUS: - return kSbMediaAudioConnectorUnknown; - case TYPE_DOCK: - return kSbMediaAudioConnectorUnknown; - case TYPE_DOCK_ANALOG: - return kSbMediaAudioConnectorAnalog; - case TYPE_FM: - return kSbMediaAudioConnectorUnknown; - case TYPE_FM_TUNER: - return kSbMediaAudioConnectorUnknown; - case TYPE_HDMI: - return kSbMediaAudioConnectorHdmi; - case TYPE_HDMI_ARC: - return kSbMediaAudioConnectorHdmi; - case TYPE_HDMI_EARC: - return kSbMediaAudioConnectorHdmi; - case TYPE_HEARING_AID: - return kSbMediaAudioConnectorUnknown; - case TYPE_IP: - return kSbMediaAudioConnectorRemoteWired; - case TYPE_LINE_ANALOG: - return kSbMediaAudioConnectorAnalog; - case TYPE_LINE_DIGITAL: - return kSbMediaAudioConnectorUnknown; - case TYPE_REMOTE_SUBMIX: - return kSbMediaAudioConnectorRemoteOther; - case TYPE_TELEPHONY: - return kSbMediaAudioConnectorUnknown; - case TYPE_TV_TUNER: - return kSbMediaAudioConnectorUnknown; - case TYPE_UNKNOWN: - return kSbMediaAudioConnectorUnknown; - case TYPE_USB_ACCESSORY: - return kSbMediaAudioConnectorUsb; - case TYPE_USB_DEVICE: - return kSbMediaAudioConnectorUsb; - case TYPE_USB_HEADSET: - return kSbMediaAudioConnectorUsb; - case TYPE_WIRED_HEADPHONES: - return kSbMediaAudioConnectorAnalog; - case TYPE_WIRED_HEADSET: - return kSbMediaAudioConnectorAnalog; - } - - SB_LOG(WARNING) << "Encountered unknown audio output device type " - << android_output_device_type; - return kSbMediaAudioConnectorUnknown; -} - // TODO(b/284140486): Refine the implementation so it works when the audio // outputs are changed during the query. bool SbMediaGetAudioConfiguration( @@ -146,56 +40,13 @@ bool SbMediaGetAudioConfiguration( return false; } - *out_configuration = {}; - - JniEnvExt* env = JniEnvExt::Get(); - ScopedLocalJavaRef j_audio_output_manager( - env->CallStarboardObjectMethodOrAbort( - "getAudioOutputManager", "()Ldev/cobalt/media/AudioOutputManager;")); - ScopedLocalJavaRef j_output_device_info(env->NewObjectOrAbort( - "dev/cobalt/media/AudioOutputManager$OutputDeviceInfo", "()V")); - - bool succeeded = env->CallBooleanMethodOrAbort( - j_audio_output_manager.Get(), "getOutputDeviceInfo", - "(ILdev/cobalt/media/AudioOutputManager$OutputDeviceInfo;)Z", - output_index, j_output_device_info.Get()); - - if (!succeeded) { - SB_LOG(WARNING) - << "Call to AudioOutputManager.getOutputDeviceInfo() failed."; - return false; - } - - auto call_int_method = [env, &j_output_device_info](const char* name) { - return env->CallIntMethodOrAbort(j_output_device_info.Get(), name, "()I"); - }; - - out_configuration->connector = - GetConnectorFromAndroidOutputType(call_int_method("getType")); - out_configuration->latency = 0; - out_configuration->coding_type = kSbMediaAudioCodingTypePcm; - out_configuration->number_of_channels = call_int_method("getChannels"); - - if (out_configuration->connector == kSbMediaAudioConnectorHdmi) { - // Keep the previous logic for HDMI to reduce risk. - // TODO(b/284140486): Update this using same logic as other connectors. - int channels = - MediaCapabilitiesCache::GetInstance()->GetMaxAudioOutputChannels(); - if (channels < 2) { - SB_LOG(WARNING) << "The supported channels from output device is " - << channels << ", set to 2 channels instead."; - out_configuration->number_of_channels = 2; - } else { - out_configuration->number_of_channels = channels; - } - } else { - out_configuration->number_of_channels = 2; - } + bool result = MediaCapabilitiesCache::GetInstance()->GetAudioConfiguration( + output_index, out_configuration); SB_LOG(INFO) << "Audio connector type for index " << output_index << " is " << GetMediaAudioConnectorName(out_configuration->connector) << " and it has " << out_configuration->number_of_channels << " channels."; - return true; + return result; } diff --git a/starboard/shared/starboard/media/mime_util.cc b/starboard/shared/starboard/media/mime_util.cc index 0ef4b2d0d51c..52650f83d472 100644 --- a/starboard/shared/starboard/media/mime_util.cc +++ b/starboard/shared/starboard/media/mime_util.cc @@ -37,23 +37,19 @@ namespace { // Use SbMediaGetAudioConfiguration() to check if the platform can support // |channels|. bool IsAudioOutputSupported(SbMediaAudioCodingType coding_type, int channels) { - // TODO(b/284140486, b/297426689): Consider removing the call to - // `SbMediaGetAudioOutputCount()` completely as the loop will be terminated - // once `SbMediaGetAudioConfiguration()` returns false. - int count = SbMediaGetAudioOutputCount(); - - for (int output_index = 0; output_index < count; ++output_index) { - SbMediaAudioConfiguration configuration; - if (!SbMediaGetAudioConfiguration(output_index, &configuration)) { - break; - } - + // SbPlayerBridge::GetAudioConfigurations() reads up to 32 configurations. The + // limit here is to avoid infinite loop and also match + // SbPlayerBridge::GetAudioConfigurations(). + const int kMaxAudioConfigurations = 32; + int output_index = 0; + SbMediaAudioConfiguration configuration; + while (output_index < kMaxAudioConfigurations && + SbMediaGetAudioConfiguration(output_index++, &configuration)) { if (configuration.coding_type == coding_type && configuration.number_of_channels >= channels) { return true; } } - return false; } From 6edfaa76040d0ce4d981588c8c5f2241b96b6f9a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:04:17 -0700 Subject: [PATCH 010/140] Cherry pick PR #901: Stop free scrolling on app hide (#1567) Refer to the original PR: https://github.com/youtube/cobalt/pull/901 b/290655439 b/290846002 Co-authored-by: Andrew Savage --- cobalt/browser/BUILD.gn | 1 - cobalt/browser/browser_module.cc | 18 +++++++ cobalt/browser/browser_module.h | 2 + cobalt/browser/splash_screen.h | 1 - cobalt/dom/html_element.cc | 3 ++ cobalt/layout/topmost_event_target.cc | 50 ++++++++++++------- .../scroll_engine/scroll_engine.cc | 8 +++ .../scroll_engine/scroll_engine.h | 14 +++++- 8 files changed, 75 insertions(+), 22 deletions(-) diff --git a/cobalt/browser/BUILD.gn b/cobalt/browser/BUILD.gn index 25e387a2aa15..9b7b12c99799 100644 --- a/cobalt/browser/BUILD.gn +++ b/cobalt/browser/BUILD.gn @@ -118,7 +118,6 @@ static_library("browser") { "client_hint_headers.h", "device_authentication.cc", "device_authentication.h", - "lifecycle_observer.h", "on_screen_keyboard_starboard_bridge.cc", "on_screen_keyboard_starboard_bridge.h", "render_tree_combiner.cc", diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc index c6f7f183613c..5a4c84669926 100644 --- a/cobalt/browser/browser_module.cc +++ b/cobalt/browser/browser_module.cc @@ -462,7 +462,9 @@ BrowserModule::~BrowserModule() { } debug_console_.reset(); #endif + DestroySplashScreen(); + DestroyScrollEngine(); // Make sure the WebModule is destroyed before the ServiceWorkerRegistry if (web_module_) { lifecycle_observers_.RemoveObserver(web_module_.get()); @@ -625,7 +627,9 @@ void BrowserModule::NavigateSetupSplashScreen( } void BrowserModule::NavigateSetupScrollEngine() { + DestroyScrollEngine(); scroll_engine_.reset(new ui_navigation::scroll_engine::ScrollEngine()); + lifecycle_observers_.AddObserver(scroll_engine_.get()); scroll_engine_->thread()->Start(); } @@ -1437,6 +1441,20 @@ void BrowserModule::DestroySplashScreen(base::TimeDelta close_time) { } } +void BrowserModule::DestroyScrollEngine() { + TRACE_EVENT0("cobalt::browser", "BrowserModule::DestroyScrollEngine()"); + if (base::MessageLoop::current() != self_message_loop_) { + self_message_loop_->task_runner()->PostTask( + FROM_HERE, base::Bind(&BrowserModule::DestroyScrollEngine, weak_this_)); + return; + } + if (scroll_engine_ && + lifecycle_observers_.HasObserver(scroll_engine_.get())) { + lifecycle_observers_.RemoveObserver(scroll_engine_.get()); + } + scroll_engine_.reset(); +} + #if defined(ENABLE_WEBDRIVER) std::unique_ptr BrowserModule::CreateSessionDriver( const webdriver::protocol::SessionId& session_id) { diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h index 201157747ff4..ab462ad9b8fa 100644 --- a/cobalt/browser/browser_module.h +++ b/cobalt/browser/browser_module.h @@ -361,6 +361,8 @@ class BrowserModule { // Destroys the splash screen, if currently displayed. void DestroySplashScreen(base::TimeDelta close_time = base::TimeDelta()); + void DestroyScrollEngine(); + // Called when web module has received window.close(). void OnWindowClose(base::TimeDelta close_time); diff --git a/cobalt/browser/splash_screen.h b/cobalt/browser/splash_screen.h index f50c2e49c24d..3fc12021a412 100644 --- a/cobalt/browser/splash_screen.h +++ b/cobalt/browser/splash_screen.h @@ -56,7 +56,6 @@ class SplashScreen : public LifecycleObserver { web_module_->SetSize(viewport_size); } - // LifecycleObserver implementation. // LifecycleObserver implementation. void Blur(SbTimeMonotonic timestamp) override { web_module_->Blur(0); } void Conceal(render_tree::ResourceProvider* resource_provider, diff --git a/cobalt/dom/html_element.cc b/cobalt/dom/html_element.cc index ef21c629d074..1aac14ef063e 100644 --- a/cobalt/dom/html_element.cc +++ b/cobalt/dom/html_element.cc @@ -1232,6 +1232,9 @@ void HTMLElement::OnUiNavFocus(SbTimeMonotonic time) { void HTMLElement::OnUiNavScroll(SbTimeMonotonic /* time */) { Document* document = node_document(); + if (document->hidden()) { + return; + } scoped_refptr window(document ? document->window() : nullptr); DispatchEvent(new UIEvent(base::Tokens::scroll(), web::Event::kNotBubbles, web::Event::kNotCancelable, window)); diff --git a/cobalt/layout/topmost_event_target.cc b/cobalt/layout/topmost_event_target.cc index 3981de4e3530..a67dd1ad7ce5 100644 --- a/cobalt/layout/topmost_event_target.cc +++ b/cobalt/layout/topmost_event_target.cc @@ -142,6 +142,15 @@ std::unique_ptr FindPossibleScrollTargets( return possible_scroll_targets; } +bool HasAnyScrollTarget( + const dom::PossibleScrollTargets* possible_scroll_targets) { + if (!possible_scroll_targets) { + return false; + } + return possible_scroll_targets->left || possible_scroll_targets->right || + possible_scroll_targets->up || possible_scroll_targets->down; +} + scoped_refptr FindFirstElementWithScrollType( dom::PossibleScrollTargets* possible_scroll_targets, ui_navigation::scroll_engine::ScrollType major_scroll_axis, @@ -280,15 +289,6 @@ void SendStateChangeLeaveEvents( } } -void SendStateChangeLeaveEvents( - bool is_pointer_event, scoped_refptr previous_element, - dom::PointerEventInit* event_init) { - scoped_refptr target_element = nullptr; - scoped_refptr nearest_common_ancestor = nullptr; - SendStateChangeLeaveEvents(is_pointer_event, previous_element, target_element, - nearest_common_ancestor, event_init); -} - void SendStateChangeEnterEvents( bool is_pointer_event, scoped_refptr previous_element, scoped_refptr target_element, @@ -420,7 +420,10 @@ void DispatchPointerEventsForScrollStart( new dom::PointerEvent(base::Tokens::pointercancel(), web::Event::kBubbles, web::Event::kNotCancelable, view, *event_init)); bool is_pointer_event = true; - SendStateChangeLeaveEvents(is_pointer_event, element, event_init); + scoped_refptr target_element = nullptr; + scoped_refptr nearest_common_ancestor = nullptr; + SendStateChangeLeaveEvents(is_pointer_event, element, target_element, + nearest_common_ancestor, event_init); } math::Matrix3F GetCompleteTransformMatrix(dom::Element* element) { @@ -563,6 +566,10 @@ void TopmostEventTarget::HandleScrollState( FindPossibleScrollTargets(target_element); pointer_state->SetPossibleScrollTargets( pointer_id, std::move(initial_possible_scroll_targets)); + if (HasAnyScrollTarget(initial_possible_scroll_targets.get())) { + pointer_state->SetPendingPointerCaptureTargetOverride(pointer_id, + target_element); + } auto transform_matrix = GetCompleteTransformMatrix(target_element.get()); pointer_state->SetClientTransformMatrix(pointer_id, transform_matrix); @@ -598,8 +605,11 @@ void TopmostEventTarget::HandleScrollState( return; } - DispatchPointerEventsForScrollStart(target_element, event_init); + scoped_refptr previous_html_element( + previous_html_element_weak_); + DispatchPointerEventsForScrollStart(previous_html_element, event_init); pointer_state->SetWasCancelled(pointer_id); + pointer_state->ClearPendingPointerCaptureTargetOverride(pointer_id); should_clear_pointer_state = true; scroll_engine_->thread()->message_loop()->task_runner()->PostTask( @@ -694,6 +704,12 @@ void TopmostEventTarget::MaybeSendPointerEvents( &event_init); } + bool event_was_cancelled = pointer_event && pointer_state->GetWasCancelled( + pointer_event->pointer_id()); + if (pointer_event && pointer_event->type() == base::Tokens::pointerup()) { + pointer_state->ClearWasCancelled(pointer_event->pointer_id()); + } + scoped_refptr previous_html_element( previous_html_element_weak_); @@ -702,14 +718,10 @@ void TopmostEventTarget::MaybeSendPointerEvents( scoped_refptr nearest_common_ancestor( GetNearestCommonAncestor(previous_html_element, target_element)); - SendStateChangeLeaveEvents(pointer_event, previous_html_element, - target_element, nearest_common_ancestor, - &event_init); - - bool event_was_cancelled = pointer_event && pointer_state->GetWasCancelled( - pointer_event->pointer_id()); - if (pointer_event && pointer_event->type() == base::Tokens::pointerup()) { - pointer_state->ClearWasCancelled(pointer_event->pointer_id()); + if (!event_was_cancelled) { + SendStateChangeLeaveEvents(pointer_event, previous_html_element, + target_element, nearest_common_ancestor, + &event_init); } if (target_element) { diff --git a/cobalt/ui_navigation/scroll_engine/scroll_engine.cc b/cobalt/ui_navigation/scroll_engine/scroll_engine.cc index 4c14f739a9f5..2c7679765374 100644 --- a/cobalt/ui_navigation/scroll_engine/scroll_engine.cc +++ b/cobalt/ui_navigation/scroll_engine/scroll_engine.cc @@ -395,6 +395,14 @@ void ScrollEngine::ScrollNavItemsWithDecayingScroll() { } } +void ScrollEngine::Conceal(render_tree::ResourceProvider*, SbTimeMonotonic) { + nav_items_with_decaying_scroll_.clear(); +} + +void ScrollEngine::Freeze(SbTimeMonotonic) { + nav_items_with_decaying_scroll_.clear(); +} + } // namespace scroll_engine } // namespace ui_navigation } // namespace cobalt diff --git a/cobalt/ui_navigation/scroll_engine/scroll_engine.h b/cobalt/ui_navigation/scroll_engine/scroll_engine.h index 93e1e33f1b8a..3e2653a47754 100644 --- a/cobalt/ui_navigation/scroll_engine/scroll_engine.h +++ b/cobalt/ui_navigation/scroll_engine/scroll_engine.h @@ -23,6 +23,7 @@ #include "base/time/time.h" #include "base/timer/timer.h" #include "cobalt/base/token.h" +#include "cobalt/browser/lifecycle_observer.h" #include "cobalt/dom/pointer_event.h" #include "cobalt/dom/pointer_event_init.h" #include "cobalt/math/vector2d_f.h" @@ -50,7 +51,7 @@ struct EventPositionWithTimeStamp { base::Time time_stamp; }; -class ScrollEngine { +class ScrollEngine : public browser::LifecycleObserver { public: ScrollEngine(); ~ScrollEngine(); @@ -73,6 +74,17 @@ class ScrollEngine { base::Thread* thread() { return &scroll_engine_; } + // LifecycleObserver implementation. + void Blur(SbTimeMonotonic timestamp) override {} + void Conceal(render_tree::ResourceProvider* resource_provider, + SbTimeMonotonic timestamp) override; + void Freeze(SbTimeMonotonic timestamp) override; + void Unfreeze(render_tree::ResourceProvider* resource_provider, + SbTimeMonotonic timestamp) override {} + void Reveal(render_tree::ResourceProvider* resource_provider, + SbTimeMonotonic timestamp) override {} + void Focus(SbTimeMonotonic timestamp) override {} + private: base::Thread scroll_engine_{"ScrollEngineThread"}; base::RepeatingTimer free_scroll_timer_; From aff4e137b43f1d3f5e0a20a1f67ade5cfd9dd558 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:05:56 -0700 Subject: [PATCH 011/140] Cherry pick PR #1302: Add EvictOldWatchdogViolations(). (#1396) Refer to the original PR: https://github.com/youtube/cobalt/pull/1302 EvictOldWatchdogViolations() evicts unfetched Watchdog violations older than 24 hours. It is called only after GetWatchdogViolations() is called. It is reponsible for ensuring that unfetched Watchdog violations do not accumulate. b/296308743 Change-Id: Ic225d80382697341f6338d554941d78930e62035 Co-authored-by: Brian Ting --- cobalt/watchdog/watchdog.cc | 86 ++++++++++++++++++++++---------- cobalt/watchdog/watchdog.h | 1 + cobalt/watchdog/watchdog_test.cc | 61 ++++++++-------------- 3 files changed, 83 insertions(+), 65 deletions(-) diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc index 1e4368810fc6..40f49151701d 100644 --- a/cobalt/watchdog/watchdog.cc +++ b/cobalt/watchdog/watchdog.cc @@ -46,6 +46,8 @@ const int64_t kWatchdogWriteWaitTime = 300000000; const int kWatchdogMaxPingInfos = 20; // The maximum length of each ping info. const int kWatchdogMaxPingInfoLength = 128; +// The maximum number of milliseconds old of an unfetched Watchdog violation. +const int64_t kWatchdogMaxViolationsAge = 86400000; // Persistent setting name and default setting for the boolean that controls // whether or not Watchdog is enabled. When disabled, Watchdog behaves like a @@ -148,6 +150,7 @@ std::string Watchdog::GetWatchdogFilePath() { } std::vector Watchdog::GetWatchdogViolationClientNames() { + starboard::ScopedLock scoped_lock(mutex_); if (pending_write_) WriteWatchdogViolations(); std::string watchdog_json = ReadViolationFile(GetWatchdogFilePath().c_str()); @@ -540,61 +543,94 @@ bool Watchdog::Ping(const std::string& name, const std::string& info) { std::string Watchdog::GetWatchdogViolations( const std::vector& clients, bool clear) { - // Gets a json string containing the Watchdog violations since the last - // call (up to the kWatchdogMaxViolations limit). + // Gets a json string containing the Watchdog violations of the given clients + // since the last call (up to the kWatchdogMaxViolations limit). if (is_disabled_) return ""; - std::string watchdog_json = ""; - std::string watchdog_json_fetched = ""; + std::string fetched_violations_json = ""; starboard::ScopedLock scoped_lock(mutex_); - if (pending_write_) WriteWatchdogViolations(); - if (!static_cast(violations_map_.get())->empty()) { - // Get all Watchdog violations if clients is given. if (clients.empty()) { - // Removes all Watchdog violations. - base::JSONWriter::Write(*violations_map_, &watchdog_json_fetched); + // Gets all Watchdog violations if no clients are given. + base::JSONWriter::Write(*violations_map_, &fetched_violations_json); if (clear) { static_cast(violations_map_.get())->Clear(); violations_count_ = 0; starboard::SbFileDeleteRecursive(GetWatchdogFilePath().c_str(), true); } } else { - base::Value filtered_client_data(base::Value::Type::DICTIONARY); - for (int i = 0; i < clients.size(); i++) { + // Gets all Watchdog violations of the given clients. + base::Value fetched_violations(base::Value::Type::DICTIONARY); + for (std::string name : clients) { base::Value* violation_dict = static_cast(violations_map_.get()) - ->FindKey(clients[i]); + ->FindKey(name); if (violation_dict != nullptr) { - filtered_client_data.SetKey(clients[i], (*violation_dict).Clone()); + fetched_violations.SetKey(name, (*violation_dict).Clone()); if (clear) { base::Value* violations = violation_dict->FindKey("violations"); int violations_count = violations->GetList().size(); static_cast(violations_map_.get()) - ->RemoveKey(clients[i]); + ->RemoveKey(name); violations_count_ -= violations_count; - if (!static_cast(violations_map_.get()) - ->empty()) { - WriteWatchdogViolations(); - } else { - starboard::SbFileDeleteRecursive(GetWatchdogFilePath().c_str(), - true); - } + pending_write_ = true; } } } - if (!filtered_client_data.DictEmpty()) { - base::JSONWriter::Write(filtered_client_data, &watchdog_json_fetched); + if (!fetched_violations.DictEmpty()) { + base::JSONWriter::Write(fetched_violations, &fetched_violations_json); + } + if (clear) { + EvictOldWatchdogViolations(); } } - SB_LOG(INFO) << "[Watchdog] Reading violations:\n" << watchdog_json_fetched; + SB_LOG(INFO) << "[Watchdog] Reading violations:\n" + << fetched_violations_json; } else { SB_LOG(INFO) << "[Watchdog] No violations."; } - return watchdog_json_fetched; + return fetched_violations_json; +} + +void Watchdog::EvictOldWatchdogViolations() { + int64_t current_timestamp_millis = SbTimeToPosix(SbTimeGetNow()) / 1000; + int64_t cutoff_timestamp_millis = + current_timestamp_millis - kWatchdogMaxViolationsAge; + std::vector empty_violations; + + // Iterates through map removing old violations. + for (const auto& map_it : violations_map_->DictItems()) { + std::string name = map_it.first; + base::Value& violation_dict = map_it.second; + base::Value* violations = violation_dict.FindKey("violations"); + for (auto list_it = violations->GetList().begin(); + list_it != violations->GetList().end();) { + int64_t violation_timestamp_millis = std::stoll( + list_it->FindKey("timestampViolationMilliseconds")->GetString()); + if (violation_timestamp_millis < cutoff_timestamp_millis) { + list_it = violations->GetList().erase(list_it); + violations_count_--; + pending_write_ = true; + } else { + list_it++; + } + } + if (violations->GetList().empty()) { + empty_violations.push_back(name); + } + } + + // Removes empty violations. + for (std::string name : empty_violations) { + static_cast(violations_map_.get())->RemoveKey(name); + } + if (static_cast(violations_map_.get())->empty()) { + starboard::SbFileDeleteRecursive(GetWatchdogFilePath().c_str(), true); + pending_write_ = false; + } } bool Watchdog::GetPersistentSettingWatchdogEnable() { diff --git a/cobalt/watchdog/watchdog.h b/cobalt/watchdog/watchdog.h index 8aa9a7cd0fbb..d8b819205ae0 100644 --- a/cobalt/watchdog/watchdog.h +++ b/cobalt/watchdog/watchdog.h @@ -106,6 +106,7 @@ class Watchdog : public Singleton { private: void WriteWatchdogViolations(); std::string ReadViolationFile(const char* file_path); + void EvictOldWatchdogViolations(); static void* Monitor(void* context); static void UpdateViolationsMap(void* context, Client* client, SbTimeMonotonic time_delta); diff --git a/cobalt/watchdog/watchdog_test.cc b/cobalt/watchdog/watchdog_test.cc index 1cafc437f849..7b4d740b96f6 100644 --- a/cobalt/watchdog/watchdog_test.cc +++ b/cobalt/watchdog/watchdog_test.cc @@ -522,13 +522,10 @@ TEST_F(WatchdogTest, GetPartialViolationsByClients) { ASSERT_TRUE(watchdog_->Register("test-name-2", "test-desc-2", base::kApplicationStateStarted, kWatchdogMonitorFrequency)); - ASSERT_TRUE(watchdog_->Register("test-name-3", "test-desc-3", - base::kApplicationStateStarted, - kWatchdogMonitorFrequency)); SbThreadSleep(kWatchdogSleepDuration); ASSERT_TRUE(watchdog_->Unregister("test-name-1")); ASSERT_TRUE(watchdog_->Unregister("test-name-2")); - ASSERT_TRUE(watchdog_->Unregister("test-name-3")); + const std::vector clients = {"test-name-1"}; std::string json = watchdog_->GetWatchdogViolations(clients); ASSERT_NE(json, ""); @@ -538,45 +535,29 @@ TEST_F(WatchdogTest, GetPartialViolationsByClients) { ASSERT_NE(violation_dict, nullptr); violation_dict = violations_map->FindKey("test-name-2"); ASSERT_EQ(violation_dict, nullptr); - violation_dict = violations_map->FindKey("test-name-3"); - ASSERT_EQ(violation_dict, nullptr); - - std::string file_json = ""; - starboard::ScopedFile read_file(watchdog_->GetWatchdogFilePath().c_str(), - kSbFileOpenOnly | kSbFileRead); - if (read_file.IsValid()) { - int64_t kFileSize = read_file.GetSize(); - std::vector buffer(kFileSize + 1, 0); - read_file.ReadAll(buffer.data(), kFileSize); - file_json = std::string(buffer.data()); - } - ASSERT_NE(file_json, ""); - violations_map = base::JSONReader::Read(file_json); - ASSERT_NE(violations_map, nullptr); - violation_dict = violations_map->FindKey("test-name-2"); - ASSERT_NE(violation_dict, nullptr); - violation_dict = violations_map->FindKey("test-name-3"); - ASSERT_NE(violation_dict, nullptr); - violation_dict = violations_map->FindKey("test-name-1"); - ASSERT_EQ(violation_dict, nullptr); - json = watchdog_->GetWatchdogViolations(clients); ASSERT_EQ(json, ""); +} - const std::vector clients2 = {"test-name-2", "test-name-3"}; - json = watchdog_->GetWatchdogViolations(clients2); - ASSERT_NE(json, ""); - violations_map = base::JSONReader::Read(json); - ASSERT_NE(violations_map, nullptr); - violation_dict = violations_map->FindKey("test-name-1"); - ASSERT_EQ(violation_dict, nullptr); - violation_dict = violations_map->FindKey("test-name-2"); - ASSERT_NE(violation_dict, nullptr); - violation_dict = violations_map->FindKey("test-name-3"); - ASSERT_NE(violation_dict, nullptr); - starboard::ScopedFile read_file_again( - watchdog_->GetWatchdogFilePath().c_str(), kSbFileOpenOnly | kSbFileRead); - ASSERT_EQ(read_file_again.IsValid(), false); +TEST_F(WatchdogTest, EvictOldWatchdogViolations) { + // Creates old Violation json file. + std::unique_ptr dummy_map = + std::make_unique(base::Value::Type::DICTIONARY); + dummy_map->SetKey("test-name-old", + CreateDummyViolationDict("test-desc-old", 0, 1)); + std::string json; + base::JSONWriter::Write(*dummy_map, &json); + starboard::ScopedFile file(watchdog_->GetWatchdogFilePath().c_str(), + kSbFileCreateAlways | kSbFileWrite); + TearDown(); + file.WriteAll(json.c_str(), static_cast(json.size())); + watchdog_ = new watchdog::Watchdog(); + watchdog_->InitializeCustom(nullptr, std::string(kWatchdogViolationsJson), + kWatchdogMonitorFrequency); + + ASSERT_NE(watchdog_->GetWatchdogViolations({}, false), ""); + ASSERT_EQ(watchdog_->GetWatchdogViolations({"test-name-new"}), ""); + ASSERT_EQ(watchdog_->GetWatchdogViolations({}, false), ""); } } // namespace watchdog From ba8ec68b59b5045c20dc024e0d0f3c3755182f83 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:44:19 -0700 Subject: [PATCH 012/140] Cherry pick PR #1422: [nplb] Relax SbMediaGetAudioConfiguration() to 3 ms (#1429) Refer to the original PR: https://github.com/youtube/cobalt/pull/1422 SbMediaConfigurationTest.ValidatePerformance() used to verify if SbMediaGetAudioConfiguration() only takes less than 0.5 milliseconds on average over 1000 runs. After https://github.com/youtube/cobalt/pull/1399 is merged, the test failed on Android TV. This CL relaxes the criterion to 3 milliseconds so the test can pass, and we'll reduce the criterion once the optimization on SbMediaGetAudioConfiguration() is done. The times of run are also reduced to 100 to reduce log spamming. b/284140486 b/297885676 Co-authored-by: xiaomings --- starboard/nplb/media_configuration_test.cc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/starboard/nplb/media_configuration_test.cc b/starboard/nplb/media_configuration_test.cc index 07c5937dfdc4..28f0dbcc652f 100644 --- a/starboard/nplb/media_configuration_test.cc +++ b/starboard/nplb/media_configuration_test.cc @@ -13,7 +13,9 @@ // limitations under the License. #include "starboard/media.h" + #include "starboard/nplb/performance_helpers.h" +#include "starboard/time.h" #include "testing/gtest/include/gtest/gtest.h" namespace starboard { @@ -25,9 +27,15 @@ TEST(SbMediaConfigurationTest, ValidatePerformance) { const int count_audio_output = SbMediaGetAudioOutputCount(); for (int i = 0; i < count_audio_output; ++i) { + constexpr int kNumberOfCalls = 100; + // TODO(b/284140486): Optimize SbMediaGetAudioConfiguration() to reduce the + // time it takes to less than 0.5 milliseconds. + constexpr SbTime kMaxAverageTimePerCall = 3 * kSbTimeMillisecond; + SbMediaAudioConfiguration configuration; - TEST_PERF_FUNCWITHARGS_DEFAULT(SbMediaGetAudioConfiguration, i, - &configuration); + TEST_PERF_FUNCWITHARGS_EXPLICIT(kNumberOfCalls, kMaxAverageTimePerCall, + SbMediaGetAudioConfiguration, i, + &configuration); } } From 3265237795e3083f4ca6ad9250714b56f1bf757f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 17:56:20 -0700 Subject: [PATCH 013/140] Cherry pick PR #1434: [android] Refine dumpAllDecoders() (#1593) Refer to the original PR: https://github.com/youtube/cobalt/pull/1434 Refine MediaCodecCapabilitiesLogger.dumpAllDecoders() with: 1. Add extra feature flags like FrameParsing, LinearBlockCopyFree, LowLatency, MultipleFrames, and PartialFrame. 2. Abstract the checking of features into TreeMap mapping from feature names to lambda functions checking for the feature. This reduces the effort to maintain two lists of features in the code. 3. Turn functions from public into private when appropriate. b/176923480 Co-authored-by: xiaomings --- .../media/MediaCodecCapabilitiesLogger.java | 169 +++++++++++++----- 1 file changed, 128 insertions(+), 41 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java index 27211118df9c..079f89784f6a 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java @@ -16,14 +16,18 @@ import static dev.cobalt.media.Log.TAG; +import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.VideoCapabilities; import android.media.MediaCodecList; +import android.os.Build; import dev.cobalt.util.Log; import java.util.ArrayList; import java.util.Arrays; import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; /** Utility class to log MediaCodec capabilities. */ public class MediaCodecCapabilitiesLogger { @@ -45,7 +49,7 @@ public boolean isCovered(Integer width, Integer height, Double frameRate) { } /** Returns a string detailing SDR and HDR capabilities of a decoder. */ - public static String getSupportedResolutionsAndFrameRates( + private static String getSupportedResolutionsAndFrameRates( VideoCapabilities videoCapabilities, boolean isHdrCapable) { ArrayList> resolutionList = new ArrayList<>( @@ -89,7 +93,7 @@ public static String getSupportedResolutionsAndFrameRates( * Like getSupportedResolutionsAndFrameRates(), but returns the full information for each frame * rate and resolution combination. */ - public static String getLongFormSupportedResolutionsAndFrameRates( + private static String getLongFormSupportedResolutionsAndFrameRates( ArrayList> resolutionList, ArrayList frameRateList, VideoCapabilities videoCapabilities, @@ -108,10 +112,10 @@ public static String getLongFormSupportedResolutionsAndFrameRates( } /** Convert a list of ResolutionAndFrameRate to a human readable string. */ - public static String convertResolutionAndFrameRatesToString( + private static String convertResolutionAndFrameRatesToString( ArrayList supported, boolean isHdrCapable) { if (supported.isEmpty()) { - return "None. "; + return "None."; } String frameRateAndResolutionString = ""; for (ResolutionAndFrameRate resolutionAndFrameRate : supported) { @@ -123,29 +127,130 @@ public static String convertResolutionAndFrameRatesToString( resolutionAndFrameRate.height, resolutionAndFrameRate.frameRate); } - frameRateAndResolutionString += isHdrCapable ? "hdr/sdr, " : "sdr, "; + frameRateAndResolutionString += isHdrCapable ? "hdr/sdr" : "sdr"; return frameRateAndResolutionString; } + private interface CodecFeatureSupported { + boolean isSupported(String name, CodecCapabilities codecCapabilities); + } + + static TreeMap featureMap; + + private static void ensurefeatureMapInitialized() { + if (featureMap != null) { + return; + } + featureMap = new TreeMap<>(); + featureMap.put( + "AdaptivePlayback", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback); + }); + featureMap.put( + "FrameParsing", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_FrameParsing); + }); + featureMap.put( + "LowLatency", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency); + }); + featureMap.put( + "MultipleFrames", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_MultipleFrames); + }); + featureMap.put( + "PartialFrame", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_PartialFrame); + }); + featureMap.put( + "LinearBlockCopyFree", + (name, codecCapabilities) -> { + if (Build.VERSION.SDK_INT < 30) { + // MediaCodec.LinearBlock is introduced in api level 30. + return false; + } + VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities(); + if (videoCapabilities == null) { + return false; + } + try { + String canonicalName = MediaCodec.createByCodecName(name).getName(); + String[] codecNames = new String[] {canonicalName}; + return MediaCodec.LinearBlock.isCodecCopyFreeCompatible(codecNames); + } catch (Exception e) { + Log.e( + TAG, + "Failed to create MediaCodec or call isCodecCopyFreeCompatible() on codec name" + + " \"%s\" with error %s", + name, + e); + return false; + } + }); + featureMap.put( + "SecurePlayback", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback); + }); + featureMap.put( + "TunneledPlayback", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback); + }); + } + + private static String getAllFeatureNames() { + ensurefeatureMapInitialized(); + return featureMap.keySet().toString(); + } + + private static String getSupportedFeaturesAsString( + String name, CodecCapabilities codecCapabilities) { + StringBuilder featuresAsString = new StringBuilder(); + + ensurefeatureMapInitialized(); + for (Map.Entry entry : featureMap.entrySet()) { + if (entry.getValue().isSupported(name, codecCapabilities)) { + if (featuresAsString.length() > 0) { + featuresAsString.append(", "); + } + featuresAsString.append(entry.getKey()); + } + } + return featuresAsString.toString(); + } + /** * Debug utility function that can be locally added to dump information about all decoders on a * particular system. */ public static void dumpAllDecoders() { - String decoderDumpString = ""; + StringBuilder decoderDumpString = new StringBuilder(); for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { if (info.isEncoder()) { continue; } for (String supportedType : info.getSupportedTypes()) { String name = info.getName(); - decoderDumpString += + decoderDumpString.append( String.format( Locale.US, - "name: %s (%s, %s): ", + "name: %s (%s, %s):", name, supportedType, - MediaCodecUtil.isCodecDenyListed(name) ? "denylisted" : "not denylisted"); + MediaCodecUtil.isCodecDenyListed(name) ? "denylisted" : "not denylisted")); CodecCapabilities codecCapabilities = info.getCapabilitiesForType(supportedType); VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities(); String resultName = @@ -160,10 +265,10 @@ public static void dumpAllDecoders() { if (videoCapabilities != null) { String frameRateAndResolutionString = getSupportedResolutionsAndFrameRates(videoCapabilities, isHdrCapable); - decoderDumpString += + decoderDumpString.append( String.format( Locale.US, - "\n\t\t" + "\n\t" + "widths: %s, " + "heights: %s, " + "bitrates: %s, " @@ -173,44 +278,26 @@ public static void dumpAllDecoders() { videoCapabilities.getSupportedHeights().toString(), videoCapabilities.getBitrateRange().toString(), videoCapabilities.getSupportedFrameRates().toString(), - frameRateAndResolutionString); + frameRateAndResolutionString)); } - boolean isAdaptivePlaybackSupported = - codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback); - boolean isSecurePlaybackSupported = - codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_SecurePlayback); - boolean isTunneledPlaybackSupported = - codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_TunneledPlayback); - if (isAdaptivePlaybackSupported - || isSecurePlaybackSupported - || isTunneledPlaybackSupported) { - decoderDumpString += - String.format( - Locale.US, - "(%s%s%s", - isAdaptivePlaybackSupported ? "AdaptivePlayback, " : "", - isSecurePlaybackSupported ? "SecurePlayback, " : "", - isTunneledPlaybackSupported ? "TunneledPlayback, " : ""); - // Remove trailing space and comma - decoderDumpString = decoderDumpString.substring(0, decoderDumpString.length() - 2); - decoderDumpString += ")"; + String featuresAsString = getSupportedFeaturesAsString(name, codecCapabilities); + if (featuresAsString.isEmpty()) { + decoderDumpString.append(" No extra features supported"); } else { - decoderDumpString += " No extra features supported"; + decoderDumpString.append("\n\tsupported features: "); + decoderDumpString.append(featuresAsString); } - decoderDumpString += "\n"; + decoderDumpString.append("\n"); } } Log.v( TAG, - " \n" + "\n" + "==================================================\n" - + "Full list of decoder features: [AdaptivePlayback, SecurePlayback," - + " TunneledPlayback]\n" - + "Unsupported features for each codec are not listed\n" - + decoderDumpString + + "Full list of decoder features: " + + getAllFeatureNames() + + "\nUnsupported features for each codec are not listed\n" + + decoderDumpString.toString() + "=================================================="); } } From 5bb80c9998c8fd322e876637a8cb3fea568bec2f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:09:37 -0700 Subject: [PATCH 014/140] Cherry pick PR #1404: Update and Clean Watchdog. (#1594) Refer to the original PR: https://github.com/youtube/cobalt/pull/1404 Update EvictWatchdogViolation to handle nullptr and act stand alone to UpdateViolationsMap if needed. Includes removing empty violations. Cleaned up and simplified Watchdog function logic removing unnecessary get() calls, i/o access, and redundant functions. violations_count_ is now rarely modified manually, a complete count of violations_maps_ is executed instead to help guarantee correctness. b/297478064 Co-authored-by: Brian Ting --- cobalt/watchdog/watchdog.cc | 182 +++++++++++++++++------------------- cobalt/watchdog/watchdog.h | 7 +- 2 files changed, 89 insertions(+), 100 deletions(-) diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc index 40f49151701d..0c7a45c900e6 100644 --- a/cobalt/watchdog/watchdog.cc +++ b/cobalt/watchdog/watchdog.cc @@ -111,7 +111,6 @@ bool Watchdog::InitializeCustom( // Starts monitor thread. is_monitoring_.store(true); - InitializeViolationsMap(this); SB_DCHECK(!SbThreadIsValid(watchdog_thread_)); watchdog_thread_ = SbThreadCreate(0, kSbThreadNoPriority, kSbThreadNoAffinity, true, "Watchdog", &Watchdog::Monitor, this); @@ -135,6 +134,29 @@ void Watchdog::Uninitialize() { SbThreadJoin(watchdog_thread_, nullptr); } +std::shared_ptr Watchdog::GetViolationsMap() { + // Gets the Watchdog violations map with lazy initialization which loads the + // previous Watchdog violations file containing violations before app start, + // if it exists. + if (violations_map_ == nullptr) { + starboard::ScopedFile read_file(GetWatchdogFilePath().c_str(), + kSbFileOpenOnly | kSbFileRead); + if (read_file.IsValid()) { + int64_t kFileSize = read_file.GetSize(); + std::vector buffer(kFileSize + 1, 0); + read_file.ReadAll(buffer.data(), kFileSize); + violations_map_ = base::JSONReader::Read(std::string(buffer.data())); + } + + if (violations_map_ == nullptr) { + SB_LOG(INFO) << "[Watchdog] No previous violations JSON."; + violations_map_ = + std::make_unique(base::Value::Type::DICTIONARY); + } + } + return violations_map_; +} + std::string Watchdog::GetWatchdogFilePath() { // Gets the Watchdog violations file path with lazy initialization. if (watchdog_file_path_ == "") { @@ -150,25 +172,28 @@ std::string Watchdog::GetWatchdogFilePath() { } std::vector Watchdog::GetWatchdogViolationClientNames() { - starboard::ScopedLock scoped_lock(mutex_); - if (pending_write_) WriteWatchdogViolations(); - - std::string watchdog_json = ReadViolationFile(GetWatchdogFilePath().c_str()); std::vector names; - if (watchdog_json != "") { - std::unique_ptr violations_map = - base::JSONReader::Read(watchdog_json); - for (const auto& it : violations_map->DictItems()) { - names.push_back(it.first); - } + + if (is_disabled_) return names; + + starboard::ScopedLock scoped_lock(mutex_); + for (const auto& it : GetViolationsMap()->DictItems()) { + names.push_back(it.first); } return names; } +void Watchdog::UpdateState(base::ApplicationState state) { + if (is_disabled_) return; + + starboard::ScopedLock scoped_lock(mutex_); + state_ = state; +} + void Watchdog::WriteWatchdogViolations() { // Writes Watchdog violations to persistent storage as a json file. std::string watchdog_json; - base::JSONWriter::Write(*violations_map_, &watchdog_json); + base::JSONWriter::Write(*GetViolationsMap(), &watchdog_json); SB_LOG(INFO) << "[Watchdog] Writing violations to JSON:\n" << watchdog_json; starboard::ScopedFile watchdog_file(GetWatchdogFilePath().c_str(), kSbFileCreateAlways | kSbFileWrite); @@ -178,13 +203,6 @@ void Watchdog::WriteWatchdogViolations() { time_last_written_microseconds_ = SbTimeGetMonotonicNow(); } -void Watchdog::UpdateState(base::ApplicationState state) { - if (is_disabled_) return; - - starboard::ScopedLock scoped_lock(mutex_); - state_ = state; -} - void* Watchdog::Monitor(void* context) { starboard::ScopedLock scoped_lock(static_cast(context)->mutex_); while (1) { @@ -238,9 +256,10 @@ void* Watchdog::Monitor(void* context) { void Watchdog::UpdateViolationsMap(void* context, Client* client, SbTimeMonotonic time_delta) { - // Gets violation dictionary with key client name from violations_map_. + // Gets violation dictionary with key client name from violations map. base::Value* violation_dict = - (static_cast(context)->violations_map_)->FindKey(client->name); + (static_cast(context)->GetViolationsMap()) + ->FindKey(client->name); // Checks if new unique violation. bool new_violation = false; @@ -259,9 +278,8 @@ void Watchdog::UpdateViolationsMap(void* context, Client* client, new_violation = true; } - // New unique violation. if (new_violation) { - // Creates new violation. + // New unique violation, creates violation in violations map. base::Value violation(base::Value::Type::DICTIONARY); violation.SetKey("pingInfos", client->ping_infos.Clone()); violation.SetKey("monitorState", @@ -292,26 +310,21 @@ void Watchdog::UpdateViolationsMap(void* context, Client* client, } violation.SetKey("registeredClients", registered_clients.Clone()); - // Adds new violation to violations_map_. + // Adds new violation to violations map. if (violation_dict == nullptr) { base::Value dict(base::Value::Type::DICTIONARY); dict.SetKey("description", base::Value(client->description)); base::Value list(base::Value::Type::LIST); list.GetList().emplace_back(violation.Clone()); dict.SetKey("violations", list.Clone()); - (static_cast(context)->violations_map_) + (static_cast(context)->GetViolationsMap()) ->SetKey(client->name, dict.Clone()); } else { base::Value* violations = violation_dict->FindKey("violations"); violations->GetList().emplace_back(violation.Clone()); } - static_cast(context)->violations_count_++; - if (static_cast(context)->violations_count_ > - kWatchdogMaxViolations) - EvictWatchdogViolation(context); - // Consecutive non-unique violation. } else { - // Updates consecutive violation in violations_map_. + // Consecutive non-unique violation, updates violation in violations map. base::Value* violations = violation_dict->FindKey("violations"); int last_index = violations->GetList().size() - 1; int64_t violation_duration = @@ -322,81 +335,68 @@ void Watchdog::UpdateViolationsMap(void* context, Client* client, "violationDurationMilliseconds", base::Value(std::to_string(violation_duration + (time_delta / 1000)))); } - static_cast(context)->pending_write_ = true; -} -std::string Watchdog::ReadViolationFile(const char* file_path) { - starboard::ScopedFile read_file(file_path, kSbFileOpenOnly | kSbFileRead); - if (read_file.IsValid()) { - int64_t kFileSize = read_file.GetSize(); - std::vector buffer(kFileSize + 1, 0); - read_file.ReadAll(buffer.data(), kFileSize); - return std::string(buffer.data()); + int violations_count = 0; + for (const auto& it : + (static_cast(context)->GetViolationsMap())->DictItems()) { + base::Value& violation_dict = it.second; + base::Value* violations = violation_dict.FindKey("violations"); + violations_count += violations->GetList().size(); } - return ""; -} - -void Watchdog::InitializeViolationsMap(void* context) { - // Loads the previous Watchdog violations file containing violations before - // app start, if it exists, to populate violations_map_. - static_cast(context)->violations_count_ = 0; - - std::string watchdog_json = - static_cast(context)->ReadViolationFile( - (static_cast(context)->GetWatchdogFilePath()).c_str()); - if (watchdog_json != "") { - static_cast(context)->violations_map_ = - base::JSONReader::Read(watchdog_json); - } - - if (static_cast(context)->violations_map_ == nullptr) { - SB_LOG(INFO) << "[Watchdog] No previous violations JSON."; - static_cast(context)->violations_map_ = - std::make_unique(base::Value::Type::DICTIONARY); - } else { - for (const auto& it : - (static_cast(context)->violations_map_)->DictItems()) { - base::Value& violation_dict = it.second; - base::Value* violations = violation_dict.FindKey("violations"); - static_cast(context)->violations_count_ += - violations->GetList().size(); - } + if (violations_count > kWatchdogMaxViolations) { + EvictWatchdogViolation(context); } } void Watchdog::EvictWatchdogViolation(void* context) { - // Evicts a violation in violations_map_ prioritizing first the most frequent + // Evicts a violation in violations map prioritizing first the most frequent // violations (largest violations count by client name) and second the oldest // violation. std::string evicted_name = ""; int evicted_count = 0; - int64_t evicted_timestamp = 0; + int64_t evicted_timestamp_millis = 0; for (const auto& it : - (static_cast(context)->violations_map_)->DictItems()) { + (static_cast(context)->GetViolationsMap())->DictItems()) { std::string name = it.first; base::Value& violation_dict = it.second; base::Value* violations = violation_dict.FindKey("violations"); int count = violations->GetList().size(); - int64_t timestamp = + int64_t violation_timestamp_millis = std::stoll(violations->GetList()[0] .FindKey("timestampViolationMilliseconds") ->GetString()); if ((evicted_name == "") || (count > evicted_count) || - ((count == evicted_count) && (timestamp < evicted_timestamp))) { + ((count == evicted_count) && + (violation_timestamp_millis < evicted_timestamp_millis))) { evicted_name = name; evicted_count = count; - evicted_timestamp = timestamp; + evicted_timestamp_millis = violation_timestamp_millis; } } base::Value* violation_dict = - (static_cast(context)->violations_map_)->FindKey(evicted_name); - base::Value* violations = violation_dict->FindKey("violations"); - violations->GetList().erase(violations->GetList().begin()); - static_cast(context)->violations_count_--; + (static_cast(context)->GetViolationsMap()) + ->FindKey(evicted_name); + + if (violation_dict != nullptr) { + base::Value* violations = violation_dict->FindKey("violations"); + violations->GetList().erase(violations->GetList().begin()); + static_cast(context)->pending_write_ = true; + + // Removes empty violations. + if (violations->GetList().empty()) { + (static_cast(context)->GetViolationsMap()) + ->RemoveKey(evicted_name); + } + if (static_cast(context)->GetViolationsMap()->DictEmpty()) { + starboard::SbFileDeleteRecursive( + static_cast(context)->GetWatchdogFilePath().c_str(), true); + static_cast(context)->pending_write_ = false; + } + } } void Watchdog::MaybeWriteWatchdogViolations(void* context) { @@ -551,31 +551,23 @@ std::string Watchdog::GetWatchdogViolations( starboard::ScopedLock scoped_lock(mutex_); - if (!static_cast(violations_map_.get())->empty()) { + if (!GetViolationsMap()->DictEmpty()) { if (clients.empty()) { // Gets all Watchdog violations if no clients are given. - base::JSONWriter::Write(*violations_map_, &fetched_violations_json); + base::JSONWriter::Write(*GetViolationsMap(), &fetched_violations_json); if (clear) { - static_cast(violations_map_.get())->Clear(); - violations_count_ = 0; + static_cast(GetViolationsMap().get())->Clear(); starboard::SbFileDeleteRecursive(GetWatchdogFilePath().c_str(), true); } } else { // Gets all Watchdog violations of the given clients. base::Value fetched_violations(base::Value::Type::DICTIONARY); for (std::string name : clients) { - base::Value* violation_dict = - static_cast(violations_map_.get()) - ->FindKey(name); + base::Value* violation_dict = GetViolationsMap()->FindKey(name); if (violation_dict != nullptr) { fetched_violations.SetKey(name, (*violation_dict).Clone()); if (clear) { - base::Value* violations = violation_dict->FindKey("violations"); - int violations_count = violations->GetList().size(); - - static_cast(violations_map_.get()) - ->RemoveKey(name); - violations_count_ -= violations_count; + GetViolationsMap()->RemoveKey(name); pending_write_ = true; } } @@ -602,7 +594,7 @@ void Watchdog::EvictOldWatchdogViolations() { std::vector empty_violations; // Iterates through map removing old violations. - for (const auto& map_it : violations_map_->DictItems()) { + for (const auto& map_it : GetViolationsMap()->DictItems()) { std::string name = map_it.first; base::Value& violation_dict = map_it.second; base::Value* violations = violation_dict.FindKey("violations"); @@ -610,9 +602,9 @@ void Watchdog::EvictOldWatchdogViolations() { list_it != violations->GetList().end();) { int64_t violation_timestamp_millis = std::stoll( list_it->FindKey("timestampViolationMilliseconds")->GetString()); + if (violation_timestamp_millis < cutoff_timestamp_millis) { list_it = violations->GetList().erase(list_it); - violations_count_--; pending_write_ = true; } else { list_it++; @@ -625,9 +617,9 @@ void Watchdog::EvictOldWatchdogViolations() { // Removes empty violations. for (std::string name : empty_violations) { - static_cast(violations_map_.get())->RemoveKey(name); + GetViolationsMap()->RemoveKey(name); } - if (static_cast(violations_map_.get())->empty()) { + if (GetViolationsMap()->DictEmpty()) { starboard::SbFileDeleteRecursive(GetWatchdogFilePath().c_str(), true); pending_write_ = false; } diff --git a/cobalt/watchdog/watchdog.h b/cobalt/watchdog/watchdog.h index d8b819205ae0..14b0c10744f2 100644 --- a/cobalt/watchdog/watchdog.h +++ b/cobalt/watchdog/watchdog.h @@ -104,13 +104,12 @@ class Watchdog : public Singleton { #endif // defined(_DEBUG) private: + std::shared_ptr GetViolationsMap(); void WriteWatchdogViolations(); - std::string ReadViolationFile(const char* file_path); void EvictOldWatchdogViolations(); static void* Monitor(void* context); static void UpdateViolationsMap(void* context, Client* client, SbTimeMonotonic time_delta); - static void InitializeViolationsMap(void* context); static void EvictWatchdogViolation(void* context); static void MaybeWriteWatchdogViolations(void* context); static void MaybeTriggerCrash(void* context); @@ -143,9 +142,7 @@ class Watchdog : public Singleton { // Dictionary of registered Watchdog clients. std::unordered_map> client_map_; // Dictionary of lists of Watchdog violations represented as dictionaries. - std::unique_ptr violations_map_; - // Number of violations in violations_map_; - int violations_count_; + std::shared_ptr violations_map_; // Monitor thread. SbThread watchdog_thread_; // Flag to stop monitor thread. From e4e2043d9619295c3efa1b855e05e9349a5d409a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:59:06 -0700 Subject: [PATCH 015/140] Cherry pick PR #1430: Add draw demo to cobalt to show pointer behavior (#1592) Refer to the original PR: https://github.com/youtube/cobalt/pull/1430 b/238814775 Co-authored-by: Andrew Savage --- cobalt/demos/content/BUILD.gn | 1 + cobalt/demos/content/draw/index.html | 56 ++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 cobalt/demos/content/draw/index.html diff --git a/cobalt/demos/content/BUILD.gn b/cobalt/demos/content/BUILD.gn index 1d6ec9a20005..9d60175856f8 100644 --- a/cobalt/demos/content/BUILD.gn +++ b/cobalt/demos/content/BUILD.gn @@ -32,6 +32,7 @@ copy("demos_testdata") { "deviceorientation-demo/deviceorientation-demo.html", "disable-jit/index.html", "dom-gc-demo/dom-gc-demo.html", + "draw/index.html", "dual-playback-demo/bear.mp4", "dual-playback-demo/dual-playback-demo.html", "eme-demo/eme-demo.html", diff --git a/cobalt/demos/content/draw/index.html b/cobalt/demos/content/draw/index.html new file mode 100644 index 000000000000..6e596c653c1d --- /dev/null +++ b/cobalt/demos/content/draw/index.html @@ -0,0 +1,56 @@ + + + + + + + + +
+ + + + + From f0c72e8037e0bffe0fd884fc2235c52e01fdd355 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:59:19 -0700 Subject: [PATCH 016/140] Cherry pick PR #1191: Invalidate scroll area cache for kImpactsBoxSizesYes (#1591) Refer to the original PR: https://github.com/youtube/cobalt/pull/1191 b/294602640 Co-authored-by: Andrew Savage --- cobalt/dom/html_element.cc | 17 +++++++++++++++++ cobalt/dom/layout_boxes.h | 5 +++++ cobalt/dom/testing/mock_layout_boxes.h | 4 ++++ cobalt/layout/layout_boxes.h | 5 +++++ 4 files changed, 31 insertions(+) diff --git a/cobalt/dom/html_element.cc b/cobalt/dom/html_element.cc index 1aac14ef063e..9ddbdcb1a8a2 100644 --- a/cobalt/dom/html_element.cc +++ b/cobalt/dom/html_element.cc @@ -161,6 +161,22 @@ struct NonTrivialStaticFields { base::LazyInstance::DestructorAtExit non_trivial_static_fields = LAZY_INSTANCE_INITIALIZER; +void InvalidateScrollAreaCacheOfAncestors(Node* node) { + for (Node* ancestor_node = node; ancestor_node; + ancestor_node = ancestor_node->parent_node()) { + Element* ancestor_element = ancestor_node->AsElement(); + if (!ancestor_element) { + continue; + } + HTMLElement* ancestor_html_element = ancestor_element->AsHTMLElement(); + if (!ancestor_html_element) { + continue; + } + if (ancestor_html_element->layout_boxes()) + ancestor_html_element->layout_boxes()->scroll_area_cache().reset(); + } +} + } // namespace void HTMLElement::RuleMatchingState::Clear() { @@ -1167,6 +1183,7 @@ void HTMLElement::InvalidateLayoutBoxesOfNodeAndDescendants() { } void HTMLElement::InvalidateLayoutBoxSizes() { + InvalidateScrollAreaCacheOfAncestors(parent_node()); if (layout_boxes_) { layout_boxes_->InvalidateSizes(); diff --git a/cobalt/dom/layout_boxes.h b/cobalt/dom/layout_boxes.h index 440e982331ea..b81a446af109 100644 --- a/cobalt/dom/layout_boxes.h +++ b/cobalt/dom/layout_boxes.h @@ -15,6 +15,8 @@ #ifndef COBALT_DOM_LAYOUT_BOXES_H_ #define COBALT_DOM_LAYOUT_BOXES_H_ +#include + #include "base/memory/ref_counted.h" #include "cobalt/dom/directionality.h" #include "cobalt/dom/dom_rect_list.h" @@ -89,6 +91,9 @@ class LayoutBoxes { // Invalidate the layout box's render tree nodes. virtual void InvalidateRenderTreeNodes() = 0; + virtual base::Optional>& + scroll_area_cache() = 0; + // Update the navigation item associated with the layout boxes. virtual void SetUiNavItem( const scoped_refptr& item) = 0; diff --git a/cobalt/dom/testing/mock_layout_boxes.h b/cobalt/dom/testing/mock_layout_boxes.h index 7065808ff108..3d00913c2d31 100644 --- a/cobalt/dom/testing/mock_layout_boxes.h +++ b/cobalt/dom/testing/mock_layout_boxes.h @@ -15,6 +15,8 @@ #ifndef COBALT_DOM_TESTING_MOCK_LAYOUT_BOXES_H_ #define COBALT_DOM_TESTING_MOCK_LAYOUT_BOXES_H_ +#include + #include "cobalt/dom/layout_boxes.h" namespace cobalt { @@ -57,6 +59,8 @@ class MockLayoutBoxes : public LayoutBoxes { MOCK_METHOD0(InvalidateSizes, void()); MOCK_METHOD0(InvalidateCrossReferences, void()); MOCK_METHOD0(InvalidateRenderTreeNodes, void()); + MOCK_METHOD0(scroll_area_cache, + base::Optional>&()); MOCK_METHOD1(SetUiNavItem, void(const scoped_refptr& item)); }; diff --git a/cobalt/layout/layout_boxes.h b/cobalt/layout/layout_boxes.h index 42ee8442ada6..919e57e64a52 100644 --- a/cobalt/layout/layout_boxes.h +++ b/cobalt/layout/layout_boxes.h @@ -67,6 +67,11 @@ class LayoutBoxes : public dom::LayoutBoxes { // const Boxes& boxes() { return boxes_; } + base::Optional>& + scroll_area_cache() override { + return scroll_area_cache_; + } + private: // Returns the bounding rectangle of the border edges of the boxes. math::RectF GetBoundingBorderRectangle() const; From e51d3e1dd67674a8e26e4d10587c1eeab402f374 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:03:13 -0700 Subject: [PATCH 017/140] Cherry pick PR #898: Add modular build for raspi (#1589) Refer to the original PR: https://github.com/youtube/cobalt/pull/898 b/288332462 Co-authored-by: Andrew Savage --- .github/config/raspi-2-modular.json | 28 +++++++ .github/workflows/raspi-2_24.lts.1+.yaml | 9 +++ starboard/BUILD.gn | 7 +- starboard/build/config/BUILDCONFIG.gn | 14 +++- starboard/build/config/install.gni | 2 +- .../build/config/starboard_target_type.gni | 8 +- .../hardfp/platform_configuration/BUILD.gn | 10 ++- .../shared/platform_configuration/BUILD.gn | 33 ++++---- starboard/linux/shared/system_get_path.cc | 27 +++++-- starboard/linux/x64x11/main.cc | 1 - .../shared/platform_configuration/BUILD.gn | 1 + starboard/raspi/2/BUILD.gn | 9 +++ .../raspi/2/platform_configuration/BUILD.gn | 19 +++-- .../platform_configuration/configuration.gni | 12 +-- starboard/raspi/2/skia/toolchain/BUILD.gn | 2 +- starboard/raspi/2/starboard_loader.cc | 19 +++++ starboard/raspi/2/toolchain/BUILD.gn | 15 ++++ starboard/raspi/2/toolchain/variables.gni | 17 ++++ starboard/raspi/shared/BUILD.gn | 29 ++++--- starboard/raspi/shared/install_target.gni | 10 +-- starboard/raspi/shared/launcher.py | 40 +++++++--- starboard/raspi/shared/main.cc | 9 --- .../shared/platform_configuration/BUILD.gn | 79 ++++++++++--------- .../platform_configuration/configuration.gni | 40 ++++++---- starboard/raspi/shared/run_starboard_main.cc | 24 ++++++ starboard/raspi/shared/test_filters.py | 15 ++++ starboard/shared/ffmpeg/BUILD.gn | 17 +++- .../starboard/player/filter/testing/BUILD.gn | 56 ++++++------- 28 files changed, 387 insertions(+), 165 deletions(-) create mode 100644 .github/config/raspi-2-modular.json create mode 100644 starboard/raspi/2/starboard_loader.cc create mode 100644 starboard/raspi/2/toolchain/variables.gni create mode 100644 starboard/raspi/shared/run_starboard_main.cc diff --git a/.github/config/raspi-2-modular.json b/.github/config/raspi-2-modular.json new file mode 100644 index 000000000000..b9a40a772c89 --- /dev/null +++ b/.github/config/raspi-2-modular.json @@ -0,0 +1,28 @@ +{ + "docker_service": "build-raspi", + "on_device_test": { + "enabled": true, + "tests": [ + "0", + "1", + "2", + "3", + "4", + "5" + ], + "test_attempts": 2 + }, + "platforms": [ + "raspi-2-modular" + ], + "includes": [ + { + "name":"modular", + "platform":"raspi-2-modular", + "target_platform":"raspi-2", + "target_cpu":"target_cpu=\\\"arm\\\"", + "extra_gn_arguments": "build_with_separate_cobalt_toolchain=true use_asan=false", + "dimension": "release_version=regex:10.*" + } + ] +} diff --git a/.github/workflows/raspi-2_24.lts.1+.yaml b/.github/workflows/raspi-2_24.lts.1+.yaml index 9eb1ec0ac329..f6918b1ae853 100644 --- a/.github/workflows/raspi-2_24.lts.1+.yaml +++ b/.github/workflows/raspi-2_24.lts.1+.yaml @@ -36,3 +36,12 @@ jobs: with: platform: raspi-2-skia nightly: ${{ github.event.inputs.nightly }} + raspi-2-modular: + uses: ./.github/workflows/main.yaml + permissions: + packages: write + pull-requests: write + with: + platform: raspi-2-modular + nightly: ${{ github.event.inputs.nightly }} + modular: true diff --git a/starboard/BUILD.gn b/starboard/BUILD.gn index 3ab468c43c09..5e5360e8013f 100644 --- a/starboard/BUILD.gn +++ b/starboard/BUILD.gn @@ -51,6 +51,7 @@ group("gn_all") { if (sb_filter_based_player) { deps += [ "//starboard/shared/starboard/player/filter/testing:player_filter_tests($starboard_toolchain)", + "//starboard/shared/starboard/player/filter/testing:player_filter_tests_install($starboard_toolchain)", "//starboard/shared/starboard/player/filter/tools:audio_dmp_player($starboard_toolchain)", ] } @@ -109,7 +110,7 @@ group("starboard") { } } else { public_deps += [ - ":starboard_platform_group($starboard_toolchain)", + ":starboard_platform_group_static($starboard_toolchain)", "//starboard/common", ] @@ -184,6 +185,10 @@ if (current_toolchain == starboard_toolchain) { starboard_platform_target("starboard_platform_group") { } + starboard_platform_target("starboard_platform_group_static") { + target_type = "group" + } + if (platform_tests_path == "") { # If 'starboard_platform_tests' is not defined by the platform, then an # empty 'starboard_platform_tests' target is defined. diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn index 4075ed303398..b60ded19243a 100644 --- a/starboard/build/config/BUILDCONFIG.gn +++ b/starboard/build/config/BUILDCONFIG.gn @@ -253,7 +253,8 @@ template("install_content") { } } - if (defined(invoker.install_content) && invoker.install_content) { + if (defined(invoker.install_content) && invoker.install_content && + current_toolchain == default_toolchain) { # We're using a custom script to copy the files here because rebase_path # can't be used with {{}} expansions in the outputs of a copy target. action("${target_name}_install_content") { @@ -294,6 +295,13 @@ template("install_content") { rebase_path(files_list, root_build_dir), ] } + } else if (current_toolchain == starboard_toolchain && + build_with_separate_cobalt_toolchain) { + outer_target_name = target_name + group("${target_name}_install_content") { + forward_variables_from(invoker, [ "testonly" ]) + deps = [ ":${outer_target_name}_install_content($default_toolchain)" ] + } } } @@ -402,6 +410,7 @@ template("shared_library") { deps = [ ":${actual_target_name}_loader($starboard_toolchain)", ":${actual_target_name}_loader_copy($starboard_toolchain)", + ":${actual_target_name}_loader_install($starboard_toolchain)", ] } if (current_toolchain == starboard_toolchain) { @@ -424,11 +433,14 @@ template("shared_library") { ldflags = [ "-Wl,-rpath=" + rebase_path("$root_build_dir/starboard"), "-Wl,-rpath=" + rebase_path("$root_build_dir"), + "-Wl,-rpath=\$ORIGIN/../lib", + "-Wl,-rpath=\$ORIGIN", ] deps = [ ":$original_target_name($cobalt_toolchain)", "//starboard:starboard_platform_group($starboard_toolchain)", + "//starboard:starboard_platform_group_install($starboard_toolchain)", ] } copy("${actual_target_name}_loader_copy") { diff --git a/starboard/build/config/install.gni b/starboard/build/config/install.gni index 9ef3f3de024f..b58479a49e6e 100644 --- a/starboard/build/config/install.gni +++ b/starboard/build/config/install.gni @@ -16,7 +16,7 @@ declare_args() { # Top-level directory for staging deploy build output. Platform install # actions should use ${sb_install_output_dir} defined in this file to place # artifacts for each deploy target in its own subdirectoy. - sb_install_output_dir = "$root_out_dir/install" + sb_install_output_dir = "$root_build_dir/install" # Sub-directory for install content. sb_install_content_subdir = "" diff --git a/starboard/build/config/starboard_target_type.gni b/starboard/build/config/starboard_target_type.gni index c1ebc178caec..c3c5916e46f8 100644 --- a/starboard/build/config/starboard_target_type.gni +++ b/starboard/build/config/starboard_target_type.gni @@ -25,13 +25,17 @@ if (starboard_target_type == "") { } template("starboard_platform_target") { - target(starboard_target_type, target_name) { + target_type = starboard_target_type + if (defined(invoker.target_type)) { + target_type = invoker.target_type + } + target(target_type, target_name) { forward_variables_from(invoker, [ "extra_configs" ]) if (defined(invoker.extra_configs)) { configs += extra_configs } - if (starboard_target_type == "shared_library") { + if (target_type == "shared_library") { build_loader = false } public_deps = [ diff --git a/starboard/evergreen/arm/hardfp/platform_configuration/BUILD.gn b/starboard/evergreen/arm/hardfp/platform_configuration/BUILD.gn index c4403eac22e1..d3474e2ef128 100644 --- a/starboard/evergreen/arm/hardfp/platform_configuration/BUILD.gn +++ b/starboard/evergreen/arm/hardfp/platform_configuration/BUILD.gn @@ -27,8 +27,10 @@ config("platform_configuration") { ":sabi_flags", "//starboard/evergreen/arm/shared/platform_configuration", ] - ldflags = [ - "-Wl,-m", - "-Wl,armelf", - ] + if (sb_is_evergreen) { + ldflags = [ + "-Wl,-m", + "-Wl,armelf", + ] + } } diff --git a/starboard/evergreen/shared/platform_configuration/BUILD.gn b/starboard/evergreen/shared/platform_configuration/BUILD.gn index 2340c86ce613..186e0f0b7352 100644 --- a/starboard/evergreen/shared/platform_configuration/BUILD.gn +++ b/starboard/evergreen/shared/platform_configuration/BUILD.gn @@ -13,20 +13,23 @@ # limitations under the License. config("platform_configuration") { - ldflags = [ - "-fuse-ld=lld", - "-Wl,--build-id", - "-Wl,--gc-sections", - "-Wl,-X", - "-Wl,-v", - "-Wl,-eh-frame-hdr", - "-Wl,--fini=__cxa_finalize", - "-Wl,-shared", - "-Wl,-L$clang_base_path", - "-Wl,-L/usr/lib", - "-Wl,-L/lib", - "-Wl,-u GetEvergreenSabiString", - ] + ldflags = [] + if (sb_is_evergreen) { + ldflags += [ + "-fuse-ld=lld", + "-Wl,--build-id", + "-Wl,--gc-sections", + "-Wl,-X", + "-Wl,-v", + "-Wl,-eh-frame-hdr", + "-Wl,--fini=__cxa_finalize", + "-Wl,-shared", + "-Wl,-L$clang_base_path", + "-Wl,-L/usr/lib", + "-Wl,-L/lib", + "-Wl,-u GetEvergreenSabiString", + ] + } if (sb_is_evergreen) { ldflags += [ "-nostdlib" ] @@ -202,7 +205,7 @@ config("speed") { config("size") { cflags = [ "-Os" ] - if (is_qa || is_gold) { + if (sb_is_evergreen && (is_qa || is_gold)) { ldflags = [ "-Wl,--icf=safe" ] } } diff --git a/starboard/linux/shared/system_get_path.cc b/starboard/linux/shared/system_get_path.cc index 0f70002e61fd..999a01609251 100644 --- a/starboard/linux/shared/system_get_path.cc +++ b/starboard/linux/shared/system_get_path.cc @@ -113,16 +113,10 @@ bool GetEvergreenContentPathOverride(char* out_path, int path_size) { } #endif -// Places up to |path_size| - 1 characters of the path to the directory -// containing the current executable in |out_path|, ensuring it is -// NULL-terminated. Returns success status. The result being greater than -// |path_size| - 1 characters is a failure. |out_path| may be written to in -// unsuccessful cases. -bool GetExecutableDirectory(char* out_path, int path_size) { - if (!GetExecutablePath(out_path, path_size)) { +bool GetParentDirectory(char* out_path) { + if (!out_path) { return false; } - char* last_slash = const_cast(strrchr(out_path, '/')); if (!last_slash) { return false; @@ -132,6 +126,18 @@ bool GetExecutableDirectory(char* out_path, int path_size) { return true; } +// Places up to |path_size| - 1 characters of the path to the directory +// containing the current executable in |out_path|, ensuring it is +// NULL-terminated. Returns success status. The result being greater than +// |path_size| - 1 characters is a failure. |out_path| may be written to in +// unsuccessful cases. +bool GetExecutableDirectory(char* out_path, int path_size) { + if (!GetExecutablePath(out_path, path_size)) { + return false; + } + return GetParentDirectory(out_path); +} + // Gets only the name portion of the current executable. bool GetExecutableName(char* out_path, int path_size) { std::vector path(kMaxPathSize, 0); @@ -168,6 +174,11 @@ bool GetContentDirectory(char* out_path, int path_size) { if (!GetExecutableDirectory(out_path, path_size)) { return false; } +#ifdef USE_COMMON_CONTENT_DIR + if (!GetParentDirectory(out_path)) { + return false; + } +#endif if (starboard::strlcat(out_path, "/content", path_size) >= path_size) { return false; } diff --git a/starboard/linux/x64x11/main.cc b/starboard/linux/x64x11/main.cc index 5cfd197c6244..19293362b2ee 100644 --- a/starboard/linux/x64x11/main.cc +++ b/starboard/linux/x64x11/main.cc @@ -50,7 +50,6 @@ extern "C" SB_EXPORT_PLATFORM int main(int argc, char** argv) { : starboard::common::GetCACertificatesPath(evergreen_content_path); if (ca_certificates_path.empty()) { SB_LOG(ERROR) << "Failed to get CA certificates path"; - return 1; } #if !SB_IS(MODULAR) diff --git a/starboard/linux/x64x11/shared/platform_configuration/BUILD.gn b/starboard/linux/x64x11/shared/platform_configuration/BUILD.gn index ab9cf36ce80e..cbdd8af7563d 100644 --- a/starboard/linux/x64x11/shared/platform_configuration/BUILD.gn +++ b/starboard/linux/x64x11/shared/platform_configuration/BUILD.gn @@ -16,6 +16,7 @@ config("platform_configuration") { if (current_toolchain == default_toolchain && sb_is_modular && !sb_is_evergreen) { configs = [ "//starboard/evergreen/x64/platform_configuration" ] + ldflags = [ "-Wl,--gc-sections" ] } else { configs = [ ":libraries", diff --git a/starboard/raspi/2/BUILD.gn b/starboard/raspi/2/BUILD.gn index 92dbb16935e1..0b84634bec94 100644 --- a/starboard/raspi/2/BUILD.gn +++ b/starboard/raspi/2/BUILD.gn @@ -22,3 +22,12 @@ static_library("starboard_platform") { configs += [ "//starboard/build/config:starboard_implementation" ] public_deps = [ "//starboard/raspi/shared:starboard_platform" ] } + +if (sb_is_modular) { + static_library("starboard_platform_with_main") { + check_includes = false + sources = [ "//starboard/raspi/shared/main.cc" ] + configs += [ "//starboard/build/config:starboard_implementation" ] + public_deps = [ ":starboard_platform" ] + } +} diff --git a/starboard/raspi/2/platform_configuration/BUILD.gn b/starboard/raspi/2/platform_configuration/BUILD.gn index ef03b60ae849..b1d042252949 100644 --- a/starboard/raspi/2/platform_configuration/BUILD.gn +++ b/starboard/raspi/2/platform_configuration/BUILD.gn @@ -17,11 +17,16 @@ config("platform_configuration") { "//starboard/build/config/sabi", "//starboard/raspi/shared/platform_configuration", ] - cflags = [ - "-march=armv7-a", - "-mfpu=neon-vfpv4", - "-mfloat-abi=hard", - "-mcpu=cortex-a8", - "-mtune=cortex-a8", - ] + if (current_toolchain != default_toolchain || !sb_is_modular) { + cflags = [ + "-march=armv7-a", + "-mfpu=neon-vfpv4", + "-mfloat-abi=hard", + "-mcpu=cortex-a8", + "-mtune=cortex-a8", + ] + } + if (sb_is_modular && !sb_is_evergreen) { + defines = [ "USE_COMMON_CONTENT_DIR" ] + } } diff --git a/starboard/raspi/2/platform_configuration/configuration.gni b/starboard/raspi/2/platform_configuration/configuration.gni index 3b236614dc46..8a175e1779e1 100644 --- a/starboard/raspi/2/platform_configuration/configuration.gni +++ b/starboard/raspi/2/platform_configuration/configuration.gni @@ -13,10 +13,12 @@ # limitations under the License. import("//starboard/raspi/shared/platform_configuration/configuration.gni") +if (current_toolchain != default_toolchain || + !build_with_separate_cobalt_toolchain) { + arm_float_abi = "hard" -arm_float_abi = "hard" + sb_evergreen_compatible_use_libunwind = true + sb_is_evergreen_compatible = true -sb_evergreen_compatible_use_libunwind = true -sb_is_evergreen_compatible = true - -separate_install_targets_for_bundling = true + separate_install_targets_for_bundling = true +} diff --git a/starboard/raspi/2/skia/toolchain/BUILD.gn b/starboard/raspi/2/skia/toolchain/BUILD.gn index 569a254e8167..3fa9a349b184 100644 --- a/starboard/raspi/2/skia/toolchain/BUILD.gn +++ b/starboard/raspi/2/skia/toolchain/BUILD.gn @@ -23,7 +23,7 @@ gcc_toolchain("target") { ar = gcc_toolchain_ar - tail_lib_dependencies = "-l:libpthread.so.0" + tail_lib_dependencies = "-l:libpthread.so.0 -l:libdl.so.2" toolchain_args = { is_clang = false diff --git a/starboard/raspi/2/starboard_loader.cc b/starboard/raspi/2/starboard_loader.cc new file mode 100644 index 000000000000..065bfaa07123 --- /dev/null +++ b/starboard/raspi/2/starboard_loader.cc @@ -0,0 +1,19 @@ +// Copyright 2023 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. + +#include "starboard/event.h" + +int main(int argc, char** argv) { + return SbRunStarboardMain(argc, argv, SbEventHandle); +} diff --git a/starboard/raspi/2/toolchain/BUILD.gn b/starboard/raspi/2/toolchain/BUILD.gn index dcce760e7f6f..676849572bda 100644 --- a/starboard/raspi/2/toolchain/BUILD.gn +++ b/starboard/raspi/2/toolchain/BUILD.gn @@ -15,6 +15,21 @@ import("//build/toolchain/gcc_toolchain.gni") import("//starboard/raspi/shared/toolchain/raspi_shared_toolchain.gni") +gcc_toolchain("starboard") { + cc = gcc_toolchain_cc + cxx = gcc_toolchain_cxx + ld = cxx + + # We use whatever 'ar' resolves to. + ar = gcc_toolchain_ar + + tail_lib_dependencies = "-l:libpthread.so.0 -l:libdl.so.2" + + toolchain_args = { + is_clang = false + } +} + gcc_toolchain("target") { cc = gcc_toolchain_cc cxx = gcc_toolchain_cxx diff --git a/starboard/raspi/2/toolchain/variables.gni b/starboard/raspi/2/toolchain/variables.gni new file mode 100644 index 000000000000..7493d34befa4 --- /dev/null +++ b/starboard/raspi/2/toolchain/variables.gni @@ -0,0 +1,17 @@ +# Copyright 2023 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. + +import("//starboard/raspi/shared/toolchain/raspi_shared_toolchain.gni") + +native_linker_path = gcc_toolchain_cxx diff --git a/starboard/raspi/shared/BUILD.gn b/starboard/raspi/shared/BUILD.gn index a81b61424a36..2ea2e8413383 100644 --- a/starboard/raspi/shared/BUILD.gn +++ b/starboard/raspi/shared/BUILD.gn @@ -41,7 +41,6 @@ static_library("starboard_platform_sources") { "//starboard/raspi/shared/dispmanx_util.h", "//starboard/raspi/shared/graphics.cc", "//starboard/raspi/shared/graphics.h", - "//starboard/raspi/shared/main.cc", "//starboard/raspi/shared/media_is_video_supported.cc", "//starboard/raspi/shared/open_max/decode_target_create.cc", "//starboard/raspi/shared/open_max/decode_target_create.h", @@ -333,10 +332,15 @@ static_library("starboard_platform_sources") { "//starboard/shared/stub/window_set_on_screen_keyboard_keep_focus.cc", "//starboard/shared/stub/window_show_on_screen_keyboard.cc", "//starboard/shared/stub/window_update_on_screen_keyboard_suggestions.cc", + "run_starboard_main.cc", ] sources += common_player_sources + if (!sb_is_modular) { + sources += [ "//starboard/raspi/shared/main.cc" ] + } + configs += [ "//starboard/build/config:starboard_implementation" ] public_deps = [ @@ -378,17 +382,20 @@ static_library("starboard_base_symbolize") { public_deps = [ "//starboard/elf_loader:evergreen_info" ] } -target(gtest_target_type, "starboard_platform_tests") { - testonly = true +if (current_toolchain == starboard_toolchain) { + target(starboard_level_gtest_target_type, "starboard_platform_tests") { + build_loader = false + testonly = true - sources = player_tests_sources + [ "//starboard/common/test_main.cc" ] + sources = player_tests_sources + [ "//starboard/common/test_main.cc" ] - configs += [ "//starboard/build/config:starboard_implementation" ] + configs += [ "//starboard/build/config:starboard_implementation" ] - deps = [ - "//starboard", - "//starboard/shared/starboard/player/filter/testing:test_util", - "//testing/gmock", - "//testing/gtest", - ] + deps = [ + "//starboard:starboard_with_main", + "//starboard/shared/starboard/player/filter/testing:test_util", + "//testing/gmock", + "//testing/gtest", + ] + } } diff --git a/starboard/raspi/shared/install_target.gni b/starboard/raspi/shared/install_target.gni index 90ac725a141a..5a281345988b 100644 --- a/starboard/raspi/shared/install_target.gni +++ b/starboard/raspi/shared/install_target.gni @@ -21,7 +21,7 @@ template("install_target") { # subdir and install content subdir. if (invoker.type == "executable") { # install_subdir = "bin" - install_subdir = "" + install_subdir = installable_target_name source_name = installable_target_name } else if (invoker.type == "shared_library") { install_subdir = "lib" @@ -42,15 +42,15 @@ template("install_target") { ] deps = invoker.deps - deps += [ ":$installable_target_name" ] + deps += [ invoker.installable_target_dep ] - outputs = [ "$sb_install_output_dir/$install_subdir/$installable_target_name/$source_name" ] + outputs = [ "$sb_install_output_dir/$install_subdir/$source_name" ] args = [ rebase_path(strip_executable, root_build_dir), "-o", - rebase_path(outputs[0], root_out_dir), - rebase_path("$root_out_dir/$source_name", root_out_dir), + rebase_path(outputs[0], root_build_dir), + rebase_path("$root_out_dir/$source_name", root_build_dir), ] } } diff --git a/starboard/raspi/shared/launcher.py b/starboard/raspi/shared/launcher.py index 6aad68dbb60b..11d7525fad6e 100644 --- a/starboard/raspi/shared/launcher.py +++ b/starboard/raspi/shared/launcher.py @@ -29,6 +29,12 @@ from starboard.tools import abstract_launcher from starboard.raspi.shared import retry +IS_MODULAR_BUILD = os.getenv('MODULAR_BUILD', '0') == '1' + + +class TargetPathError(ValueError): + pass + # pylint: disable=unused-argument def _sigint_or_sigterm_handler(signum, frame): @@ -126,31 +132,43 @@ def __init__(self, platform, target_name, config, device_id, **kwargs): self.last_run_pexpect_cmd = '' + def _GetAndCheckTestFile(self, target_name): + # TODO(b/218889313): This should reference the bin/ subdir when that's + # used. + test_dir = os.path.join(self.out_directory, 'install', target_name) + test_file = target_name + test_path = os.path.join(test_dir, test_file) + + if not os.path.isfile(test_path): + raise TargetPathError(f'TargetPath ({test_path}) must be a file.') + return test_file + + def _GetAndCheckTestFileWithFallback(self): + try: + return self._GetAndCheckTestFile(self.target_name + '_loader') + except TargetPathError as e: + if IS_MODULAR_BUILD: + raise e + return self._GetAndCheckTestFile(self.target_name) + def _InitPexpectCommands(self): """Initializes all of the pexpect commands needed for running the test.""" # Ensure no trailing slashes self.out_directory = self.out_directory.rstrip('/') - # TODO(b/218889313): This should reference the bin/ subdir when that's - # used. - test_dir = os.path.join(self.out_directory, 'install', self.target_name) - test_file = self.target_name - - test_path = os.path.join(test_dir, test_file) - if not os.path.isfile(test_path): - raise ValueError(f'TargetPath ({test_path}) must be a file.') + test_file = self._GetAndCheckTestFileWithFallback() raspi_user_hostname = Launcher._RASPI_USERNAME + '@' + self.device_id # Use the basename of the out directory as a common directory on the device # so content can be reused for several targets w/o re-syncing for each one. raspi_test_dir = os.path.basename(self.out_directory) - raspi_test_path = os.path.join(raspi_test_dir, test_file) + raspi_test_path = os.path.join(raspi_test_dir, test_file, test_file) # rsync command setup - options = '-avzLhc' - source = test_dir + '/' + options = '-avzLh' + source = os.path.join(self.out_directory, 'install') + '/' destination = f'{raspi_user_hostname}:~/{raspi_test_dir}/' self.rsync_command = 'rsync ' + options + ' ' + source + ' ' + destination diff --git a/starboard/raspi/shared/main.cc b/starboard/raspi/shared/main.cc index e0aa5bfe4019..b07e33ef1299 100644 --- a/starboard/raspi/shared/main.cc +++ b/starboard/raspi/shared/main.cc @@ -49,7 +49,6 @@ int main(int argc, char** argv) { : starboard::common::GetCACertificatesPath(evergreen_content_path); if (ca_certificates_path.empty()) { SB_LOG(ERROR) << "Failed to get CA certificates path"; - return 1; } bool start_handler_at_crash = @@ -72,11 +71,3 @@ int main(int argc, char** argv) { starboard::shared::signal::UninstallCrashSignalHandlers(); return result; } - -#if SB_API_VERSION >= 15 -int SbRunStarboardMain(int argc, char** argv, SbEventHandleCallback callback) { - starboard::raspi::shared::ApplicationDispmanx application(callback); - int result = application.Run(argc, argv); - return result; -} -#endif // SB_API_VERSION >= 15 diff --git a/starboard/raspi/shared/platform_configuration/BUILD.gn b/starboard/raspi/shared/platform_configuration/BUILD.gn index b1c930ed3a14..e2c7f035d31b 100644 --- a/starboard/raspi/shared/platform_configuration/BUILD.gn +++ b/starboard/raspi/shared/platform_configuration/BUILD.gn @@ -16,6 +16,39 @@ declare_args() { raspi_home = getenv("RASPI_HOME") } +config("common_flags") { + ldflags = [ + "--sysroot=$raspi_home/busterroot", + + # This is a quirk of Raspbian, these are required to link any GL-related + # libraries. + "-L$raspi_home/busterroot/opt/vc/lib", + "-Wl,-rpath=$raspi_home/busterroot/opt/vc/lib", + "-L$raspi_home/busterroot/usr/lib/arm-linux-gnueabihf", + "-Wl,-rpath=$raspi_home/busterroot/usr/lib/arm-linux-gnueabihf", + "-L$raspi_home/busterroot/lib/arm-linux-gnueabihf", + "-Wl,-rpath=$raspi_home/busterroot/lib/arm-linux-gnueabihf", + + # Cleanup unused sections + "-Wl,-gc-sections", + "-Wl,--unresolved-symbols=ignore-all", + ] + libs = [ + "asound", + "rt", + "openmaxil", + "bcm_host", + "vcos", + "vchiq_arm", + "brcmGLESv2", + "brcmEGL", + + # Static libs must be last, to avoid __dlopen linker errors + "EGL_static", + "GLESv2_static", + ] +} + config("compiler_flags") { cflags = [] cflags_c = [] @@ -38,23 +71,6 @@ config("compiler_flags") { cflags += [ "-Wno-unused-but-set-variable" ] } - ldflags += [ - "--sysroot=$raspi_home/busterroot", - - # This is a quirk of Raspbian, these are required to link any GL-related - # libraries. - "-L$raspi_home/busterroot/opt/vc/lib", - "-Wl,-rpath=$raspi_home/busterroot/opt/vc/lib", - "-L$raspi_home/busterroot/usr/lib/arm-linux-gnueabihf", - "-Wl,-rpath=$raspi_home/busterroot/usr/lib/arm-linux-gnueabihf", - "-L$raspi_home/busterroot/lib/arm-linux-gnueabihf", - "-Wl,-rpath=$raspi_home/busterroot/lib/arm-linux-gnueabihf", - - # Cleanup unused sections - "-Wl,-gc-sections", - "-Wl,--unresolved-symbols=ignore-in-shared-libs", - ] - cflags += [ # Generated by code in the raspi/shared/open_max. "-Wno-sign-compare", @@ -121,27 +137,16 @@ config("compiler_flags") { } config("platform_configuration") { - libs = [ - "asound", - "dl", - "pthread", - "rt", - "openmaxil", - "bcm_host", - "vcos", - "vchiq_arm", - "brcmGLESv2", - "brcmEGL", - - # Static libs must be last, to avoid __dlopen linker errors - "EGL_static", - "GLESv2_static", - ] - - configs = [ "//starboard/raspi/shared/platform_configuration:compiler_flags" ] + configs = [ ":common_flags" ] + if (current_toolchain == default_toolchain && sb_is_modular) { + configs += [ "//starboard/evergreen/arm/hardfp/platform_configuration" ] + } else { + configs += + [ "//starboard/raspi/shared/platform_configuration:compiler_flags" ] - if (is_debug || is_devel) { - configs += [ "//build/config/compiler:rtti" ] + if (is_debug || is_devel) { + configs += [ "//build/config/compiler:rtti" ] + } } } diff --git a/starboard/raspi/shared/platform_configuration/configuration.gni b/starboard/raspi/shared/platform_configuration/configuration.gni index bd06d1810a04..b1e053e8db04 100644 --- a/starboard/raspi/shared/platform_configuration/configuration.gni +++ b/starboard/raspi/shared/platform_configuration/configuration.gni @@ -12,26 +12,36 @@ # See the License for the specific language governing permissions and # limitations under the License. -import("//starboard/build/config/base_configuration.gni") +if (current_toolchain == default_toolchain && + build_with_separate_cobalt_toolchain) { + import( + "//starboard/evergreen/arm/softfp/platform_configuration/configuration.gni") -arm_float_abi = "hard" + platform_tests_path = "//starboard/raspi/shared:starboard_platform_tests_install($starboard_toolchain)" + cobalt_font_package = "standard" +} else { + import("//starboard/build/config/base_configuration.gni") -sb_static_contents_output_data_dir = "$root_out_dir/content" + arm_float_abi = "hard" -no_pedantic_warnings_config_path = - "//starboard/raspi/shared/platform_configuration:no_pedantic_warnings" -pedantic_warnings_config_path = - "//starboard/raspi/shared/platform_configuration:pedantic_warnings" -sabi_path = "//starboard/sabi/arm/hardfp/sabi-v$sb_api_version.json" + sb_static_contents_output_data_dir = "$root_out_dir/content" -platform_tests_path = "//starboard/raspi/shared:starboard_platform_tests" + no_pedantic_warnings_config_path = + "//starboard/raspi/shared/platform_configuration:no_pedantic_warnings" + pedantic_warnings_config_path = + "//starboard/raspi/shared/platform_configuration:pedantic_warnings" + sabi_path = "//starboard/sabi/arm/hardfp/sabi-v$sb_api_version.json" -install_target_path = "//starboard/raspi/shared/install_target.gni" + platform_tests_path = "//starboard/raspi/shared:starboard_platform_tests_install($starboard_toolchain)" + + speed_config_path = "//starboard/raspi/shared/platform_configuration:speed" + size_config_path = "//starboard/raspi/shared/platform_configuration:size" -speed_config_path = "//starboard/raspi/shared/platform_configuration:speed" -size_config_path = "//starboard/raspi/shared/platform_configuration:size" + # TODO(b/219073252): Enable -fno-exceptions and don't mix it with -fexceptions. + enable_exceptions_override = true -# TODO(b/219073252): Enable -fno-exceptions and don't mix it with -fexceptions. -enable_exceptions_override = true + v8_enable_webassembly = true -v8_enable_webassembly = true + is_raspi = true +} +install_target_path = "//starboard/raspi/shared/install_target.gni" diff --git a/starboard/raspi/shared/run_starboard_main.cc b/starboard/raspi/shared/run_starboard_main.cc new file mode 100644 index 000000000000..9e957afcb394 --- /dev/null +++ b/starboard/raspi/shared/run_starboard_main.cc @@ -0,0 +1,24 @@ +// Copyright 2016 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. + +#include "starboard/event.h" +#include "starboard/raspi/shared/application_dispmanx.h" + +#if SB_API_VERSION >= 15 +int SbRunStarboardMain(int argc, char** argv, SbEventHandleCallback callback) { + starboard::raspi::shared::ApplicationDispmanx application(callback); + int result = application.Run(argc, argv); + return result; +} +#endif // SB_API_VERSION >= 15 diff --git a/starboard/raspi/shared/test_filters.py b/starboard/raspi/shared/test_filters.py index d031a5de7c6d..d6aeb5549d06 100644 --- a/starboard/raspi/shared/test_filters.py +++ b/starboard/raspi/shared/test_filters.py @@ -13,9 +13,22 @@ # limitations under the License. """Starboard Raspberry Pi Platform Test Filters.""" +import os from starboard.tools.testing import test_filter # pylint: disable=line-too-long +_MODULAR_BUILD_FILTERED_TESTS = { + 'nplb': [ + 'SbSystemGetStackTest.SunnyDayStackDirection', + 'SbSystemGetStackTest.SunnyDay', + 'SbSystemGetStackTest.SunnyDayShortStack', + 'SbSystemSymbolizeTest.SunnyDay' + 'MemoryReportingTest.CapturesOperatorDeleteNothrow', + 'SbAudioSinkTest.*', 'SbDrmTest.AnySupportedKeySystems' + ], + 'player_filter_tests': [test_filter.FILTER_ALL], +} + _FILTERED_TESTS = { 'nplb': [ 'SbAudioSinkTest.*', @@ -49,6 +62,8 @@ 'PlayerComponentsTests/PlayerComponentsTest.*', ], } +if os.getenv('MODULAR_BUILD', '0') == '1': + _FILTERED_TESTS = _MODULAR_BUILD_FILTERED_TESTS class TestFilters(object): diff --git a/starboard/shared/ffmpeg/BUILD.gn b/starboard/shared/ffmpeg/BUILD.gn index ec30571dcf9c..5ad46938f1a9 100644 --- a/starboard/shared/ffmpeg/BUILD.gn +++ b/starboard/shared/ffmpeg/BUILD.gn @@ -18,18 +18,29 @@ ffmpeg_specialization_sources = [ "ffmpeg_common.h", "ffmpeg_demuxer_impl.cc", "ffmpeg_demuxer_impl.h", - "ffmpeg_video_decoder_impl.cc", - "ffmpeg_video_decoder_impl.h", ] +if (!defined(is_raspi)) { + ffmpeg_specialization_sources += [ + # TODO(b/291783511): This code is unimplemented on certain platforms. + "ffmpeg_video_decoder_impl.cc", + "ffmpeg_video_decoder_impl.h", + ] +} + static_library("ffmpeg_dynamic_load") { check_includes = false sources = [ "ffmpeg_dynamic_load_audio_decoder_impl.cc", "ffmpeg_dynamic_load_demuxer_impl.cc", "ffmpeg_dynamic_load_dispatch_impl.cc", - "ffmpeg_dynamic_load_video_decoder_impl.cc", ] + if (!defined(is_raspi)) { + sources += [ + # TODO(b/291783511): This code is unimplemented on certain platforms. + "ffmpeg_dynamic_load_video_decoder_impl.cc", + ] + } public_deps = [ ":ffmpeg.57.107.100", ":ffmpeg.58.35.100", diff --git a/starboard/shared/starboard/player/filter/testing/BUILD.gn b/starboard/shared/starboard/player/filter/testing/BUILD.gn index 9d6f6b9df387..d1cb5bedc103 100644 --- a/starboard/shared/starboard/player/filter/testing/BUILD.gn +++ b/starboard/shared/starboard/player/filter/testing/BUILD.gn @@ -49,41 +49,41 @@ if (current_toolchain == starboard_toolchain) { data_deps = [ "//starboard/shared/starboard/player:player_download_test_data" ] } -} -if (host_os != "win" && current_toolchain == starboard_toolchain) { - target(final_executable_type, "player_filter_benchmarks") { - testonly = true + if (host_os != "win") { + target(final_executable_type, "player_filter_benchmarks") { + testonly = true - sources = [ - "//starboard/common/benchmark_main.cc", - "audio_decoder_benchmark.cc", - ] + sources = [ + "//starboard/common/benchmark_main.cc", + "audio_decoder_benchmark.cc", + ] - public_deps = [ - ":test_util", - "//third_party/google_benchmark", - ] + public_deps = [ + ":test_util", + "//third_party/google_benchmark", + ] - deps = cobalt_platform_dependencies + deps = cobalt_platform_dependencies + } } -} -static_library("test_util") { - testonly = true + static_library("test_util") { + testonly = true - sources = [ - "test_util.cc", - "test_util.h", - ] + sources = [ + "test_util.cc", + "test_util.h", + ] - public_configs = [ "//starboard/build/config:starboard_implementation" ] + public_configs = [ "//starboard/build/config:starboard_implementation" ] - public_deps = [ - "//starboard", - "//starboard/shared/starboard/media:media_util", - "//starboard/shared/starboard/player:player_download_test_data", - "//starboard/shared/starboard/player:video_dmp", - "//testing/gtest", - ] + public_deps = [ + "//starboard", + "//starboard/shared/starboard/media:media_util", + "//starboard/shared/starboard/player:player_download_test_data", + "//starboard/shared/starboard/player:video_dmp", + "//testing/gtest", + ] + } } From fa6e8f4e377a7cd70489f4c0503102d5d1fbbf37 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 19:07:45 -0700 Subject: [PATCH 018/140] Cherry pick PR #1425: Move retry logic to individual readline calls (#1590) Refer to the original PR: https://github.com/youtube/cobalt/pull/1425 b/297905836 --------- Co-authored-by: Oscar Vestlie --- starboard/raspi/shared/launcher.py | 45 ++++++++++++++---------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/starboard/raspi/shared/launcher.py b/starboard/raspi/shared/launcher.py index 11d7525fad6e..6aa6c46ef0fc 100644 --- a/starboard/raspi/shared/launcher.py +++ b/starboard/raspi/shared/launcher.py @@ -89,8 +89,8 @@ class Launcher(abstract_launcher.AbstractLauncher): _PROMPT_WAIT_MAX_RETRIES = 5 # Wait up to 10 seconds for the password prompt from the raspi _PEXPECT_PASSWORD_TIMEOUT_MAX_RETRIES = 10 - # Wait up to 900 seconds for new output from the raspi - _PEXPECT_READLINE_TIMEOUT_MAX_RETRIES = 900 + # Wait up to 600 seconds for new output from the raspi + _PEXPECT_READLINE_TIMEOUT_MAX_RETRIES = 600 # Delay between subsequent SSH commands _INTER_COMMAND_DELAY_SECONDS = 1.5 @@ -260,28 +260,25 @@ def _PexpectSendLine(self, cmd): def _PexpectReadLines(self): """Reads all lines from the pexpect process.""" - # pylint: disable=unnecessary-lambda - @retry.retry( - exceptions=Launcher._RETRY_EXCEPTIONS, - retries=Launcher._PEXPECT_READLINE_TIMEOUT_MAX_RETRIES, - backoff=lambda: self.shutdown_initiated.is_set(), - wrap_exceptions=False) - def _readloop(): - while True: - # Sanitize the line to remove ansi color codes. - line = Launcher._PEXPECT_SANITIZE_LINE_RE.sub( - '', self.pexpect_process.readline()) - self.output_file.flush() - if not line: - return - # Check for the test complete tag. It will be followed by either a - # success or failure tag. - if line.startswith(self.test_complete_tag): - if line.find(self.test_success_tag) != -1: - self.return_value = 0 - return - - _readloop() + while True: + # pylint: disable=unnecessary-lambda + line = retry.with_retry( + self.pexpect_process.readline, + exceptions=Launcher._RETRY_EXCEPTIONS, + retries=Launcher._PEXPECT_READLINE_TIMEOUT_MAX_RETRIES, + backoff=lambda: self.shutdown_initiated.is_set(), + wrap_exceptions=False) + # Sanitize the line to remove ansi color codes. + line = Launcher._PEXPECT_SANITIZE_LINE_RE.sub('', line) + self.output_file.flush() + if not line: + return + # Check for the test complete tag. It will be followed by either a + # success or failure tag. + if line.startswith(self.test_complete_tag): + if line.find(self.test_success_tag) != -1: + self.return_value = 0 + return def _Sleep(self, val): self._PexpectSendLine(f'sleep {val};echo {Launcher._SSH_SLEEP_SIGNAL}') From 68d49a3bfc7ebf840074bf90f064edfdd53b4430 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 19 Sep 2023 20:47:24 -0700 Subject: [PATCH 019/140] Cherry pick PR #1397: Only strip raspi symbols for gold builds (#1597) Refer to the original PR: https://github.com/youtube/cobalt/pull/1397 b/297277024 Co-authored-by: Andrew Savage --- starboard/raspi/shared/install_target.gni | 43 ++++++++++++++--------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/starboard/raspi/shared/install_target.gni b/starboard/raspi/shared/install_target.gni index 5a281345988b..2c0f4e1d8d3d 100644 --- a/starboard/raspi/shared/install_target.gni +++ b/starboard/raspi/shared/install_target.gni @@ -29,28 +29,37 @@ template("install_target") { } else { assert(false, "You can only install an executable or shared library.") } + output = "$sb_install_output_dir/$install_subdir/$source_name" + input = "$root_out_dir/$source_name" - action(target_name) { - forward_variables_from(invoker, [ "testonly" ]) + if (is_gold) { + action(target_name) { + forward_variables_from(invoker, [ "testonly" ]) - script = "//starboard/build/run_bash.py" + script = "//starboard/build/run_bash.py" - strip_executable = gcc_toolchain_strip - inputs = [ - strip_executable, - "$root_out_dir/$source_name", - ] + strip_executable = gcc_toolchain_strip + inputs = [ input ] - deps = invoker.deps - deps += [ invoker.installable_target_dep ] + deps = invoker.deps + deps += [ invoker.installable_target_dep ] - outputs = [ "$sb_install_output_dir/$install_subdir/$source_name" ] + outputs = [ output ] - args = [ - rebase_path(strip_executable, root_build_dir), - "-o", - rebase_path(outputs[0], root_build_dir), - rebase_path("$root_out_dir/$source_name", root_build_dir), - ] + args = [ + rebase_path(strip_executable, root_build_dir), + "-o", + rebase_path(outputs[0], root_build_dir), + rebase_path(inputs[0], root_build_dir), + ] + } + } else { + copy(target_name) { + forward_variables_from(invoker, [ "testonly" ]) + sources = [ input ] + outputs = [ output ] + deps = invoker.deps + deps += [ invoker.installable_target_dep ] + } } } From 67164b3c9287be24cf86508f726784ab4a77ba57 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 06:00:27 -0700 Subject: [PATCH 020/140] Cherry pick PR #995: Remove TODO to enable variations tests (#1241) Refer to the original PR: https://github.com/youtube/cobalt/pull/995 I've come to realize //components/variations is all Finch logic, which is entirely unused and disabled in Cobalt. Enabling these tests has low value until the day comes we decide to use Finch for experiments. b/283258321 Co-authored-by: Joel Martinez --- components/variations/BUILD.gn | 1 - 1 file changed, 1 deletion(-) diff --git a/components/variations/BUILD.gn b/components/variations/BUILD.gn index ad64baf04992..dd6fa7bfce30 100644 --- a/components/variations/BUILD.gn +++ b/components/variations/BUILD.gn @@ -128,7 +128,6 @@ if (is_android && !use_cobalt_customizations) { } } -# TODO(b/283258321): Re-enable as many tests as posible. if (!use_cobalt_customizations) { static_library("test_support") { testonly = true From 6de7a11d4080857729b9e41b2f2f1e5b0954743d Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 06:04:15 -0700 Subject: [PATCH 021/140] Cherry pick PR #1439: Attempt to resolve some dangling pointer issues in Telemetry (#1511) Refer to the original PR: https://github.com/youtube/cobalt/pull/1439 b/298057575 b/298050585 Co-authored-by: Joel Martinez --- .../metrics/cobalt_metrics_log_uploader.cc | 9 +++-- .../metrics/cobalt_metrics_service_client.cc | 11 ++++++ .../metrics/cobalt_metrics_service_client.h | 5 +++ .../cobalt_metrics_services_manager.cc | 36 ++++++++++++++++--- .../metrics/cobalt_metrics_services_manager.h | 11 ++++++ cobalt/h5vcc/h5vcc_metrics.cc | 25 ++++++++++--- cobalt/h5vcc/h5vcc_metrics.h | 2 ++ 7 files changed, 87 insertions(+), 12 deletions(-) diff --git a/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc b/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc index 49f1c8164923..2c8088377ec8 100644 --- a/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc +++ b/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc @@ -67,8 +67,13 @@ void CobaltMetricsLogUploader::UploadLog( base::Base64UrlEncode(cobalt_uma_event.SerializeAsString(), base::Base64UrlEncodePolicy::INCLUDE_PADDING, &base64_encoded_proto); - upload_handler_->Run(h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma, - base64_encoded_proto); + // Check again that the upload handler is still valid. Was seeing race + // conditions where it was being destroyed while the proto encoding was + // happening above. + if (upload_handler_ != nullptr) { + upload_handler_->Run(h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma, + base64_encoded_proto); + } } } diff --git a/cobalt/browser/metrics/cobalt_metrics_service_client.cc b/cobalt/browser/metrics/cobalt_metrics_service_client.cc index 2b4d21aebb76..f52ac1d85803 100644 --- a/cobalt/browser/metrics/cobalt_metrics_service_client.cc +++ b/cobalt/browser/metrics/cobalt_metrics_service_client.cc @@ -58,6 +58,17 @@ void CobaltMetricsServiceClient::SetOnUploadHandler( } } +void CobaltMetricsServiceClient::RemoveOnUploadHandler( + const CobaltMetricsUploaderCallback* uploader_callback) { + // Only remove the upload handler if our current reference matches that which + // is passed in. Avoids issues with race conditions with two threads trying to + // override the handler. + if (upload_handler_ == uploader_callback) { + LOG(INFO) << "Upload handler removed."; + upload_handler_ = nullptr; + } +} + CobaltMetricsServiceClient::CobaltMetricsServiceClient( ::metrics::MetricsStateManager* state_manager, PrefService* local_state) : metrics_state_manager_(state_manager) { diff --git a/cobalt/browser/metrics/cobalt_metrics_service_client.h b/cobalt/browser/metrics/cobalt_metrics_service_client.h index c7860f0c57b5..7ba9a7cd9df5 100644 --- a/cobalt/browser/metrics/cobalt_metrics_service_client.h +++ b/cobalt/browser/metrics/cobalt_metrics_service_client.h @@ -53,6 +53,11 @@ class CobaltMetricsServiceClient : public ::metrics::MetricsServiceClient { void SetOnUploadHandler( const CobaltMetricsUploaderCallback* uploader_callback); + // Remove reference to the passed uploader callback, if it's the current + // reference. Otherwise, does nothing. + void RemoveOnUploadHandler( + const CobaltMetricsUploaderCallback* uploader_callback); + // Returns the MetricsService instance that this client is associated with. // With the exception of testing contexts, the returned instance must be valid // for the lifetime of this object (typically, the embedder's client diff --git a/cobalt/browser/metrics/cobalt_metrics_services_manager.cc b/cobalt/browser/metrics/cobalt_metrics_services_manager.cc index acb5d8bd2c63..a17f7b0d1fbe 100644 --- a/cobalt/browser/metrics/cobalt_metrics_services_manager.cc +++ b/cobalt/browser/metrics/cobalt_metrics_services_manager.cc @@ -41,14 +41,40 @@ CobaltMetricsServicesManager* CobaltMetricsServicesManager::GetInstance() { return instance_; } -void CobaltMetricsServicesManager::DeleteInstance() { delete instance_; } +void CobaltMetricsServicesManager::DeleteInstance() { + delete instance_; + instance_ = nullptr; +} + +void CobaltMetricsServicesManager::RemoveOnUploadHandler( + const CobaltMetricsUploaderCallback* uploader_callback) { + if (instance_ != nullptr) { + instance_->task_runner_->PostTask( + FROM_HERE, + base::Bind(&CobaltMetricsServicesManager::RemoveOnUploadHandlerInternal, + base::Unretained(instance_), uploader_callback)); + } +} + +void CobaltMetricsServicesManager::RemoveOnUploadHandlerInternal( + const CobaltMetricsUploaderCallback* uploader_callback) { + CobaltMetricsServiceClient* client = + static_cast(GetMetricsServiceClient()); + DCHECK(client); + client->RemoveOnUploadHandler(uploader_callback); +} void CobaltMetricsServicesManager::SetOnUploadHandler( const CobaltMetricsUploaderCallback* uploader_callback) { - instance_->task_runner_->PostTask( - FROM_HERE, - base::Bind(&CobaltMetricsServicesManager::SetOnUploadHandlerInternal, - base::Unretained(instance_), uploader_callback)); + // H5vccMetrics calls this on destruction when the WebModule is torn down. On + // shutdown, CobaltMetricsServicesManager can be destructed before + // H5vccMetrics, so we make sure we have a valid instance here. + if (instance_ != nullptr) { + instance_->task_runner_->PostTask( + FROM_HERE, + base::Bind(&CobaltMetricsServicesManager::SetOnUploadHandlerInternal, + base::Unretained(instance_), uploader_callback)); + } } void CobaltMetricsServicesManager::SetOnUploadHandlerInternal( diff --git a/cobalt/browser/metrics/cobalt_metrics_services_manager.h b/cobalt/browser/metrics/cobalt_metrics_services_manager.h index da31294ca474..4df4d13a47a4 100644 --- a/cobalt/browser/metrics/cobalt_metrics_services_manager.h +++ b/cobalt/browser/metrics/cobalt_metrics_services_manager.h @@ -59,6 +59,14 @@ class CobaltMetricsServicesManager static void SetOnUploadHandler( const CobaltMetricsUploaderCallback* uploader_callback); + // Attempts to clean up the passed reference to CobaltMetricsUploaderCallback, + // IFF it matches the current callback reference in + // CobaltMetricsServiceManager. This is to avoid situations where two clients + // are competing to override the upload handler and prevent one from + // inadvertently clobbering another. + static void RemoveOnUploadHandler( + const CobaltMetricsUploaderCallback* uploader_callback); + // Toggles whether metric reporting is enabled via // CobaltMetricsServicesManager. static void ToggleMetricsEnabled(bool is_enabled); @@ -71,6 +79,9 @@ class CobaltMetricsServicesManager void SetOnUploadHandlerInternal( const CobaltMetricsUploaderCallback* uploader_callback); + void RemoveOnUploadHandlerInternal( + const CobaltMetricsUploaderCallback* uploader_callback); + void ToggleMetricsEnabledInternal(bool is_enabled); void SetUploadIntervalInternal(uint32_t interval_seconds); diff --git a/cobalt/h5vcc/h5vcc_metrics.cc b/cobalt/h5vcc/h5vcc_metrics.cc index 0c9062a6bb34..ce5e7842172e 100644 --- a/cobalt/h5vcc/h5vcc_metrics.cc +++ b/cobalt/h5vcc/h5vcc_metrics.cc @@ -25,6 +25,17 @@ namespace cobalt { namespace h5vcc { +H5vccMetrics::~H5vccMetrics() { + if (browser::metrics::CobaltMetricsServicesManager::GetInstance() != + nullptr && + run_event_handler_callback_) { + // We need to let the metrics manager know not to call the upload callback + // any longer, otherwise it could crash. + browser::metrics::CobaltMetricsServicesManager::GetInstance() + ->RemoveOnUploadHandler(run_event_handler_callback_.get()); + } +} + void H5vccMetrics::OnMetricEvent( const h5vcc::MetricEventHandlerWrapper::ScriptValue& event_handler) { if (!uploader_callback_) { @@ -43,16 +54,20 @@ void H5vccMetrics::OnMetricEvent( void H5vccMetrics::RunEventHandler( const cobalt::h5vcc::H5vccMetricType& metric_type, const std::string& serialized_proto) { - task_runner_->PostTask( - FROM_HERE, - base::Bind(&H5vccMetrics::RunEventHandlerInternal, base::Unretained(this), - metric_type, serialized_proto)); + if (task_runner_ && task_runner_->HasAtLeastOneRef()) { + task_runner_->PostTask( + FROM_HERE, + base::Bind(&H5vccMetrics::RunEventHandlerInternal, + base::Unretained(this), metric_type, serialized_proto)); + } } void H5vccMetrics::RunEventHandlerInternal( const cobalt::h5vcc::H5vccMetricType& metric_type, const std::string& serialized_proto) { - uploader_callback_->callback.value().Run(metric_type, serialized_proto); + if (uploader_callback_ != nullptr && uploader_callback_->HasAtLeastOneRef()) { + uploader_callback_->callback.value().Run(metric_type, serialized_proto); + } } void H5vccMetrics::Enable() { ToggleMetricsEnabled(true); } diff --git a/cobalt/h5vcc/h5vcc_metrics.h b/cobalt/h5vcc/h5vcc_metrics.h index a8f69ea920b4..61e98078265f 100644 --- a/cobalt/h5vcc/h5vcc_metrics.h +++ b/cobalt/h5vcc/h5vcc_metrics.h @@ -47,6 +47,8 @@ class H5vccMetrics : public script::Wrappable { : task_runner_(base::ThreadTaskRunnerHandle::Get()), persistent_settings_(persistent_settings) {} + ~H5vccMetrics(); + H5vccMetrics(const H5vccMetrics&) = delete; H5vccMetrics& operator=(const H5vccMetrics&) = delete; From 9f80681507100ce16714c04c693ad84c1910026e Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 07:53:04 -0700 Subject: [PATCH 022/140] Cherry pick PR #1577: Enable crashpad_database_util to be built (#1600) Refer to the original PR: https://github.com/youtube/cobalt/pull/1577 The "native_target" toolchain is used to build this tool since it shouldn't need Starboard. b/251521595 Change-Id: I2f0fa416c63f3e659a48db95291ead13b530d9af Co-authored-by: Holden Warriner --- starboard/BUILD.gn | 1 + starboard/doc/evergreen/cobalt_evergreen_overview.md | 4 ++-- third_party/crashpad/build/BUILD.gn | 9 +++++++++ third_party/crashpad/handler/BUILD.gn | 11 +---------- third_party/crashpad/tools/BUILD.gn | 8 +++++--- 5 files changed, 18 insertions(+), 15 deletions(-) diff --git a/starboard/BUILD.gn b/starboard/BUILD.gn index 5e5360e8013f..3c9fc83c9b2b 100644 --- a/starboard/BUILD.gn +++ b/starboard/BUILD.gn @@ -72,6 +72,7 @@ group("gn_all") { data_deps = [ "//starboard/loader_app($starboard_toolchain)", "//third_party/crashpad/handler:crashpad_handler(//$starboard_path/toolchain:native_target)", + "//third_party/crashpad/tools:crashpad_database_util(//$starboard_path/toolchain:native_target)", ] } } diff --git a/starboard/doc/evergreen/cobalt_evergreen_overview.md b/starboard/doc/evergreen/cobalt_evergreen_overview.md index 2f35e8b5469d..f258cbbe7e8b 100644 --- a/starboard/doc/evergreen/cobalt_evergreen_overview.md +++ b/starboard/doc/evergreen/cobalt_evergreen_overview.md @@ -305,8 +305,8 @@ instructions available [here](cobalt_evergreen_reference_port_raspi2.md). 1. Build the `crashpad_database_util` target and deploy it onto the device. ``` -$ cobalt/build/gn.py -p -c qa -$ ninja -C out/_qa crashpad_database_util +$ gn gen out/_qa --args='target_platform="" build_type="qa"' +$ ninja -C out/_qa native_target/crashpad_database_util ``` 2. Remove the existing state for crashpad as it throttles uploads to 1 per hour: ``` diff --git a/third_party/crashpad/build/BUILD.gn b/third_party/crashpad/build/BUILD.gn index 6e6ccd647261..e80f9b5d1249 100644 --- a/third_party/crashpad/build/BUILD.gn +++ b/third_party/crashpad/build/BUILD.gn @@ -66,3 +66,12 @@ if (crashpad_is_ios) { } } } + +if (crashpad_is_in_native_target_build) { + config("native_target_executable_config") { + # This is to undo the "main=StarboardMain" define added for all targets + # when final_executable_type == "shared_library", in + # starboard/build/config/BUILD.gn, which itself is admittedly a hack. + defines = [ "StarboardMain=main" ] + } +} diff --git a/third_party/crashpad/handler/BUILD.gn b/third_party/crashpad/handler/BUILD.gn index b8ce718c5f2c..2b9d2e4c12e1 100644 --- a/third_party/crashpad/handler/BUILD.gn +++ b/third_party/crashpad/handler/BUILD.gn @@ -169,15 +169,6 @@ if (!crashpad_is_ios) { } } - if (crashpad_is_in_native_target_build) { - config("crashpad_handler_native_target_config") { - # This is to undo the "main=StarboardMain" define added for all targets - # when final_executable_type == "shared_library", in - # starboard/build/config/BUILD.gn, which itself is admittedly a hack. - defines = [ "StarboardMain=main" ] - } - } - crashpad_executable("crashpad_handler") { if (crashpad_is_in_starboard) { install_target = !crashpad_is_android @@ -212,7 +203,7 @@ if (!crashpad_is_ios) { } if (crashpad_is_in_native_target_build) { - configs += [ ":crashpad_handler_native_target_config" ] + configs += [ "../build:native_target_executable_config" ] } } } diff --git a/third_party/crashpad/tools/BUILD.gn b/third_party/crashpad/tools/BUILD.gn index 402f04add6c3..76d6d286decb 100644 --- a/third_party/crashpad/tools/BUILD.gn +++ b/third_party/crashpad/tools/BUILD.gn @@ -31,9 +31,7 @@ source_set("tool_support") { } } -# TODO(b/251521595): resolve GN error and enable this target to be built for -# android. -if (!crashpad_is_ios && !crashpad_is_android) { +if (!crashpad_is_ios) { crashpad_executable("crashpad_database_util") { check_includes = !crashpad_is_in_starboard && !crashpad_is_in_native_target_build @@ -53,6 +51,10 @@ if (!crashpad_is_ios && !crashpad_is_android) { "//starboard", ] } + + if (crashpad_is_in_native_target_build) { + configs = [ "../build:native_target_executable_config" ] + } } if (!crashpad_is_in_starboard && !crashpad_is_in_native_target_build) { From b58bb4e2e8569a2baa9e36e0a05b81716f4c99a7 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 08:10:44 -0700 Subject: [PATCH 023/140] Cherry pick PR #1501: Strip raspi builds in CI (#1599) Refer to the original PR: https://github.com/youtube/cobalt/pull/1501 b/299352457 Co-authored-by: Andrew Savage --- starboard/raspi/shared/install_target.gni | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starboard/raspi/shared/install_target.gni b/starboard/raspi/shared/install_target.gni index 2c0f4e1d8d3d..741384015ba6 100644 --- a/starboard/raspi/shared/install_target.gni +++ b/starboard/raspi/shared/install_target.gni @@ -32,7 +32,7 @@ template("install_target") { output = "$sb_install_output_dir/$install_subdir/$source_name" input = "$root_out_dir/$source_name" - if (is_gold) { + if (is_gold || cobalt_fastbuild) { action(target_name) { forward_variables_from(invoker, [ "testonly" ]) From b4f9aa08ebc3248e42843af63a1e8c47ba229925 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:23:30 -0700 Subject: [PATCH 024/140] Cherry pick PR #1584: Add back crashpad handler installation for AOSP-EG (#1601) Refer to the original PR: https://github.com/youtube/cobalt/pull/1584 It looks like this was accidentally removed for Starboard versions where modular toolchains are supported. b/265874860 Change-Id: I6dd4608b1575176128d45446bb026a3d1e73b883 Co-authored-by: Holden Warriner --- starboard/android/shared/android_main.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/starboard/android/shared/android_main.cc b/starboard/android/shared/android_main.cc index dbdb8c074057..4477656ccdef 100644 --- a/starboard/android/shared/android_main.cc +++ b/starboard/android/shared/android_main.cc @@ -407,6 +407,10 @@ extern "C" int SbRunStarboardMain(int argc, CommandLine command_line(GetArgs()); LogInit(command_line); +#if SB_IS(EVERGREEN_COMPATIBLE) + InstallCrashpadHandler(command_line); +#endif // SB_IS(EVERGREEN_COMPATIBLE) + // Mark the app running before signaling app created so there's no race to // allow sending the first AndroidCommand after onCreate() returns. g_app_running = true; From 4fd259ec3cde05995df7a03db843f5548b840a95 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:38:54 -0700 Subject: [PATCH 025/140] Cherry pick PR #1413: Clear video surface without re-initializing EGL. (#1437) Refer to the original PR: https://github.com/youtube/cobalt/pull/1413 * Move clearing of the video surface initiated in video decoder destructor to Java, removing the need to re-initialize EGL. * Move clearing of initial surface to wrapped eglMakeCurrent. b/297264187 Co-authored-by: Jelle Foks --- .../java/dev/cobalt/coat/CobaltActivity.java | 4 + .../java/dev/cobalt/coat/StarboardBridge.java | 9 ++ .../dev/cobalt/media/VideoSurfaceView.java | 16 ++ starboard/android/shared/BUILD.gn | 2 +- starboard/android/shared/system_egl.cc | 146 ++++++++++++++++++ starboard/android/shared/video_window.cc | 116 ++------------ 6 files changed, 192 insertions(+), 101 deletions(-) create mode 100644 starboard/android/shared/system_egl.cc diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java index 2a763280dd1d..cb3a65b67c2d 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java @@ -328,6 +328,10 @@ public void onRequestPermissionsResult( getStarboardBridge().onRequestPermissionsResult(requestCode, permissions, grantResults); } + public void clearVideoSurface() { + if (videoSurfaceView != null) videoSurfaceView.clearSurface(); + } + public void resetVideoSurface() { runOnUiThread( new Runnable() { diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java index 61f4bca10aa1..d63645b7fe49 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java @@ -689,6 +689,15 @@ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] gra audioPermissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults); } + @SuppressWarnings("unused") + @UsedByNative + public void clearVideoSurface() { + Activity activity = activityHolder.get(); + if (activity instanceof CobaltActivity) { + ((CobaltActivity) activity).clearVideoSurface(); + } + } + @SuppressWarnings("unused") @UsedByNative public void resetVideoSurface() { diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java index a4ef40ea4b1f..339e36365453 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java @@ -17,7 +17,10 @@ import static dev.cobalt.media.Log.TAG; import android.content.Context; +import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.PixelFormat; +import android.graphics.PorterDuff; import android.os.Build; import android.util.AttributeSet; import android.view.Surface; @@ -75,6 +78,19 @@ private void initialize(Context context) { // punch-out video when the position / size is animated. } + public void clearSurface() { + if (getHolder().getSurface().isValid()) { + Canvas canvas = getHolder().lockCanvas(); + if (canvas != null) { + canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); + getHolder().unlockCanvasAndPost(canvas); + } + // Trigger a surface changed event to prevent 'already connected'. + getHolder().setFormat(PixelFormat.TRANSPARENT); + getHolder().setFormat(PixelFormat.OPAQUE); + } + } + private static native void nativeOnVideoSurfaceChanged(Surface surface); private static native void nativeSetNeedResetSurface(); diff --git a/starboard/android/shared/BUILD.gn b/starboard/android/shared/BUILD.gn index b07982aab008..13585f33d31d 100644 --- a/starboard/android/shared/BUILD.gn +++ b/starboard/android/shared/BUILD.gn @@ -52,7 +52,6 @@ action("game_activity_sources") { static_library("starboard_platform") { sources = [ - "//starboard/shared/egl/system_egl.cc", "//starboard/shared/gcc/atomic_gcc_public.h", "//starboard/shared/gles/gl_call.h", "//starboard/shared/gles/system_gles2.cc", @@ -365,6 +364,7 @@ static_library("starboard_platform") { "speech_synthesis_internal.cc", "speech_synthesis_is_supported.cc", "speech_synthesis_speak.cc", + "system_egl.cc", "system_get_connection_type.cc", "system_get_device_type.cc", "system_get_extensions.cc", diff --git a/starboard/android/shared/system_egl.cc b/starboard/android/shared/system_egl.cc new file mode 100644 index 000000000000..9a81788e27bf --- /dev/null +++ b/starboard/android/shared/system_egl.cc @@ -0,0 +1,146 @@ +// Copyright 2019 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. + +#include +#include +#include + +#include "starboard/egl.h" +#include "starboard/gles.h" + +#if !defined(EGL_VERSION_1_0) || !defined(EGL_VERSION_1_1) || \ + !defined(EGL_VERSION_1_2) || !defined(EGL_VERSION_1_3) || \ + !defined(EGL_VERSION_1_4) +#error "EGL version must be >= 1.4" +#endif + +namespace { + +bool first_make_current = false; + +EGLBoolean SbEglInitialize(EGLDisplay dpy, EGLint* major, EGLint* minor) { + first_make_current = true; + return eglInitialize(dpy, major, minor); +} + +EGLBoolean SbEglTerminate(EGLDisplay dpy) { + first_make_current = false; + return eglTerminate(dpy); +} + +EGLBoolean SbEglMakeCurrent(EGLDisplay dpy, + EGLSurface draw, + EGLSurface read, + EGLContext ctx) { + EGLBoolean result = eglMakeCurrent(dpy, draw, read, ctx); + if (first_make_current && (dpy != EGL_NO_DISPLAY) && + (draw != EGL_NO_SURFACE) && (eglGetError() == EGL_SUCCESS)) { + first_make_current = false; + // Start by showing a black surface immediately. + const SbGlesInterface* gles = SbGetGlesInterface(); + gles->glClearColor(0, 0, 0, 1); + gles->glClear(GL_COLOR_BUFFER_BIT); + gles->glFlush(); + if (glGetError() == GL_NO_ERROR) { + eglSwapBuffers(dpy, draw); + } + } + return result; +} + +// Convenience functions that redirect to the intended function but "cast" the +// type of the SbEglNative*Type parameter into the desired type. Depending on +// the platform, the type of cast to use is different so either C-style casts or +// constructor-style casts are needed to work across platforms (or provide +// implementations for these functions for each platform). + +SbEglBoolean SbEglCopyBuffers(SbEglDisplay dpy, + SbEglSurface surface, + SbEglNativePixmapType target) { + return eglCopyBuffers(dpy, surface, (EGLNativePixmapType)target); +} + +SbEglSurface SbEglCreatePixmapSurface(SbEglDisplay dpy, + SbEglConfig config, + SbEglNativePixmapType pixmap, + const SbEglInt32* attrib_list) { + return eglCreatePixmapSurface(dpy, config, (EGLNativePixmapType)pixmap, + attrib_list); +} + +SbEglSurface SbEglCreateWindowSurface(SbEglDisplay dpy, + SbEglConfig config, + SbEglNativeWindowType win, + const SbEglInt32* attrib_list) { + return eglCreateWindowSurface(dpy, config, (EGLNativeWindowType)win, + attrib_list); +} + +SbEglDisplay SbEglGetDisplay(SbEglNativeDisplayType display_id) { + return eglGetDisplay((EGLNativeDisplayType)display_id); +} + +const SbEglInterface g_sb_egl_interface = { + &eglChooseConfig, + &SbEglCopyBuffers, + &eglCreateContext, + &eglCreatePbufferSurface, + &SbEglCreatePixmapSurface, + &SbEglCreateWindowSurface, + &eglDestroyContext, + &eglDestroySurface, + &eglGetConfigAttrib, + &eglGetConfigs, + &eglGetCurrentDisplay, + &eglGetCurrentSurface, + &SbEglGetDisplay, + &eglGetError, + &eglGetProcAddress, + &SbEglInitialize, + &SbEglMakeCurrent, + &eglQueryContext, + &eglQueryString, + &eglQuerySurface, + &eglSwapBuffers, + &SbEglTerminate, + &eglWaitGL, + &eglWaitNative, + &eglBindTexImage, + &eglReleaseTexImage, + &eglSurfaceAttrib, + &eglSwapInterval, + &eglBindAPI, + &eglQueryAPI, + &eglCreatePbufferFromClientBuffer, + &eglReleaseThread, + &eglWaitClient, + &eglGetCurrentContext, + + nullptr, // eglCreateSync + nullptr, // eglDestroySync + nullptr, // eglClientWaitSync + nullptr, // eglGetSyncAttrib + nullptr, // eglCreateImage + nullptr, // eglDestroyImage + nullptr, // eglGetPlatformDisplay + nullptr, // eglCreatePlatformWindowSurface + nullptr, // eglCreatePlatformPixmapSurface + nullptr, // eglWaitSync +}; + +} // namespace + +const SbEglInterface* SbGetEglInterface() { + return &g_sb_egl_interface; +} diff --git a/starboard/android/shared/video_window.cc b/starboard/android/shared/video_window.cc index 5e401ecd1e8a..15237ec6164d 100644 --- a/starboard/android/shared/video_window.cc +++ b/starboard/android/shared/video_window.cc @@ -44,90 +44,6 @@ VideoSurfaceHolder* g_video_surface_holder = NULL; // Global boolean to indicate if we need to reset SurfaceView after playing // vertical video. bool g_reset_surface_on_clear_window = false; - -void ClearNativeWindow(ANativeWindow* native_window) { - EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); - eglInitialize(display, NULL, NULL); - if (display == EGL_NO_DISPLAY) { - SB_DLOG(ERROR) << "Found no EGL display in ClearVideoWindow"; - return; - } - - const EGLint kAttributeList[] = { - EGL_RED_SIZE, - 8, - EGL_GREEN_SIZE, - 8, - EGL_BLUE_SIZE, - 8, - EGL_ALPHA_SIZE, - 8, - EGL_RENDERABLE_TYPE, - EGL_OPENGL_ES2_BIT, - EGL_NONE, - 0, - EGL_NONE, - }; - - // First, query how many configs match the given attribute list. - EGLint num_configs = 0; - EGL_CALL(eglChooseConfig(display, kAttributeList, NULL, 0, &num_configs)); - SB_DCHECK(num_configs != 0); - - // Allocate space to receive the matching configs and retrieve them. - EGLConfig* configs = new EGLConfig[num_configs]; - EGL_CALL(eglChooseConfig(display, kAttributeList, configs, num_configs, - &num_configs)); - - EGLNativeWindowType egl_native_window = - static_cast(native_window); - EGLConfig config; - - // Find the first config that successfully allow a window surface to be - // created. - EGLSurface surface; - for (int config_number = 0; config_number < num_configs; ++config_number) { - config = configs[config_number]; - surface = eglCreateWindowSurface(display, config, egl_native_window, NULL); - if (eglGetError() == EGL_SUCCESS) - break; - } - if (surface == EGL_NO_SURFACE) { - SB_DLOG(ERROR) << "Found no EGL surface in ClearVideoWindow"; - return; - } - SB_DCHECK(surface != EGL_NO_SURFACE); - - delete[] configs; - - // Create an OpenGL ES 2.0 context. - EGLContext context = EGL_NO_CONTEXT; - EGLint context_attrib_list[] = { - EGL_CONTEXT_CLIENT_VERSION, - 2, - EGL_NONE, - }; - context = - eglCreateContext(display, config, EGL_NO_CONTEXT, context_attrib_list); - SB_DCHECK(eglGetError() == EGL_SUCCESS); - SB_DCHECK(context != EGL_NO_CONTEXT); - - /* connect the context to the surface */ - EGL_CALL(eglMakeCurrent(display, surface, surface, context)); - - GL_CALL(glClearColor(0, 0, 0, 1)); - GL_CALL(glClear(GL_COLOR_BUFFER_BIT)); - GL_CALL(glFlush()); - EGL_CALL(eglSwapBuffers(display, surface)); - - // Cleanup all used resources. - EGL_CALL( - eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); - EGL_CALL(eglDestroyContext(display, context)); - EGL_CALL(eglDestroySurface(display, surface)); - EGL_CALL(eglTerminate(display)); -} - } // namespace extern "C" SB_EXPORT_PLATFORM void @@ -151,7 +67,6 @@ Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged( if (surface) { g_j_video_surface = env->NewGlobalRef(surface); g_native_video_window = ANativeWindow_fromSurface(env, surface); - ClearNativeWindow(g_native_video_window); } } @@ -204,28 +119,29 @@ bool VideoSurfaceHolder::GetVideoWindowSize(int* width, int* height) { void VideoSurfaceHolder::ClearVideoWindow(bool force_reset_surface) { // Lock *GetViewSurfaceMutex() here, to avoid releasing g_native_video_window // during painting. - ScopedLock lock(*GetViewSurfaceMutex()); + { + ScopedLock lock(*GetViewSurfaceMutex()); - if (!g_native_video_window) { - SB_LOG(INFO) << "Tried to clear video window when it was null."; - return; - } + if (!g_native_video_window) { + SB_LOG(INFO) << "Tried to clear video window when it was null."; + return; + } - if (force_reset_surface) { - JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", - "()V"); - return; - } else if (g_reset_surface_on_clear_window) { - int width = ANativeWindow_getWidth(g_native_video_window); - int height = ANativeWindow_getHeight(g_native_video_window); - if (width <= height) { + if (force_reset_surface) { JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", "()V"); return; + } else if (g_reset_surface_on_clear_window) { + int width = ANativeWindow_getWidth(g_native_video_window); + int height = ANativeWindow_getHeight(g_native_video_window); + if (width <= height) { + JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", + "()V"); + return; + } } } - - ClearNativeWindow(g_native_video_window); + JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("clearVideoSurface", "()V"); } } // namespace shared From e2aedc3c3cbbb83853c5db11053a8d411c4087c6 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:46:30 -0700 Subject: [PATCH 026/140] Cherry pick PR #1539: Add defensive null checks and fix unprotected signal. (#1574) Refer to the original PR: https://github.com/youtube/cobalt/pull/1539 Hold the mutex while signaling the thread in the destructor, and add a few defensive null checks around the AudioFrameDiscarder that may be related to the short stack crashes. This may also help with ANRs although I did not find a matching one. b/298054820 Co-authored-by: Jelle Foks --- starboard/android/shared/audio_decoder.cc | 22 +++++++++++++------ starboard/android/shared/media_decoder.cc | 12 +++++++--- .../ffmpeg/ffmpeg_audio_decoder_impl.cc | 7 ++++-- .../shared/libfdkaac/fdk_aac_audio_decoder.cc | 4 ++++ .../player/filter/audio_frame_discarder.cc | 8 +++++-- 5 files changed, 39 insertions(+), 14 deletions(-) diff --git a/starboard/android/shared/audio_decoder.cc b/starboard/android/shared/audio_decoder.cc index dfa32dd488e7..60e7a44a88e6 100644 --- a/starboard/android/shared/audio_decoder.cc +++ b/starboard/android/shared/audio_decoder.cc @@ -94,9 +94,10 @@ void AudioDecoder::Initialize(const OutputCB& output_cb, output_cb_ = output_cb; error_cb_ = error_cb; - - media_decoder_->Initialize( - std::bind(&AudioDecoder::ReportError, this, _1, _2)); + if (media_decoder_) { + media_decoder_->Initialize( + std::bind(&AudioDecoder::ReportError, this, _1, _2)); + } } void AudioDecoder::Decode(const InputBuffers& input_buffers, @@ -108,15 +109,20 @@ void AudioDecoder::Decode(const InputBuffers& input_buffers, audio_frame_discarder_.OnInputBuffers(input_buffers); +#if STARBOARD_ANDROID_SHARED_AUDIO_DECODER_VERBOSE for (const auto& input_buffer : input_buffers) { VERBOSE_MEDIA_LOG() << "T1: timestamp " << input_buffer->timestamp(); } +#endif - media_decoder_->WriteInputBuffers(input_buffers); + if (media_decoder_) { + media_decoder_->WriteInputBuffers(input_buffers); + } ScopedLock lock(decoded_audios_mutex_); - if (media_decoder_->GetNumberOfPendingTasks() + decoded_audios_.size() <= - kMaxPendingWorkSize) { + if (media_decoder_ && + (media_decoder_->GetNumberOfPendingTasks() + decoded_audios_.size() <= + kMaxPendingWorkSize)) { Schedule(consumed_cb); } else { consumed_cb_ = consumed_cb; @@ -128,7 +134,9 @@ void AudioDecoder::WriteEndOfStream() { SB_DCHECK(output_cb_); SB_DCHECK(media_decoder_); - media_decoder_->WriteEndOfStream(); + if (media_decoder_) { + media_decoder_->WriteEndOfStream(); + } } scoped_refptr AudioDecoder::Read( diff --git a/starboard/android/shared/media_decoder.cc b/starboard/android/shared/media_decoder.cc index 78425b7a03b1..74bb4a738bdc 100644 --- a/starboard/android/shared/media_decoder.cc +++ b/starboard/android/shared/media_decoder.cc @@ -146,9 +146,11 @@ MediaDecoder::MediaDecoder(Host* host, MediaDecoder::~MediaDecoder() { SB_DCHECK(thread_checker_.CalledOnValidThread()); - destroying_.store(true); - condition_variable_.Signal(); + { + ScopedLock scoped_lock(mutex_); + condition_variable_.Signal(); + } if (SbThreadIsValid(decoder_thread_)) { SbThreadJoin(decoder_thread_, NULL); @@ -184,11 +186,15 @@ void MediaDecoder::Initialize(const ErrorCB& error_cb) { void MediaDecoder::WriteInputBuffers(const InputBuffers& input_buffers) { SB_DCHECK(thread_checker_.CalledOnValidThread()); - SB_DCHECK(!input_buffers.empty()); if (stream_ended_.load()) { SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called."; return; } + if (input_buffers.empty()) { + SB_LOG(ERROR) << "No input buffer to decode."; + SB_DCHECK(!input_buffers.empty()); + return; + } if (!SbThreadIsValid(decoder_thread_)) { decoder_thread_ = SbThreadCreate( diff --git a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc index 4cf444bd2caf..1086f3f4f435 100644 --- a/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc +++ b/starboard/shared/ffmpeg/ffmpeg_audio_decoder_impl.cc @@ -140,14 +140,17 @@ void AudioDecoderImpl::Decode(const InputBuffers& input_buffers, SB_DCHECK(output_cb_); SB_CHECK(codec_context_ != NULL); - const auto& input_buffer = input_buffers[0]; - Schedule(consumed_cb); + if (input_buffers.empty() || !input_buffers[0]) { + SB_LOG(ERROR) << "No input buffer to decode."; + return; + } if (stream_ended_) { SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called."; return; } + const auto& input_buffer = input_buffers[0]; AVPacket packet; ffmpeg_->av_init_packet(&packet); diff --git a/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc index 693c13313a3c..1df5b8b2e3d1 100644 --- a/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc +++ b/starboard/shared/libfdkaac/fdk_aac_audio_decoder.cc @@ -52,6 +52,10 @@ void FdkAacAudioDecoder::Decode(const InputBuffers& input_buffers, SB_DCHECK(output_cb_); SB_DCHECK(decoder_ != NULL); + if (input_buffers.empty() || !input_buffers[0]) { + SB_LOG(ERROR) << "No input buffer to decode."; + return; + } if (stream_ended_) { SB_LOG(ERROR) << "Decode() is called after WriteEndOfStream() is called."; return; diff --git a/starboard/shared/starboard/player/filter/audio_frame_discarder.cc b/starboard/shared/starboard/player/filter/audio_frame_discarder.cc index b1ad4fb2304e..b8f2c36a9888 100644 --- a/starboard/shared/starboard/player/filter/audio_frame_discarder.cc +++ b/starboard/shared/starboard/player/filter/audio_frame_discarder.cc @@ -43,8 +43,12 @@ void AudioFrameDiscarder::OnInputBuffers(const InputBuffers& input_buffers) { void AudioFrameDiscarder::AdjustForDiscardedDurations( int sample_rate, scoped_refptr* decoded_audio) { - SB_DCHECK(decoded_audio); - SB_DCHECK(*decoded_audio); + if (!decoded_audio || !*decoded_audio) { + SB_LOG(ERROR) << "No input buffer to adjust."; + SB_DCHECK(decoded_audio); + SB_DCHECK(*decoded_audio); + return; + } InputBufferInfo input_info; { From 0d6c7622ffa101ff5a73f9df265e0f7007e176d6 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:47:17 -0700 Subject: [PATCH 027/140] Cherry pick PR #1427: Remove bare MessageLoop pointer in h5vcc runtime. (#1436) Refer to the original PR: https://github.com/youtube/cobalt/pull/1427 Instead of holding a bare pointer to the MessageLoop, hold a reference counted pointer to the task runner for ensuring deep link events are handled in the right sequence, avoiding crashes when deep links are received after a message loop becomes invalid but before the h5vcc object is destroyed. b/283503512 Co-authored-by: Jelle Foks --- cobalt/h5vcc/h5vcc_runtime.cc | 8 +++++--- cobalt/h5vcc/h5vcc_runtime.h | 7 ++++--- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cobalt/h5vcc/h5vcc_runtime.cc b/cobalt/h5vcc/h5vcc_runtime.cc index 15f682be0188..1ec7fcc2903e 100644 --- a/cobalt/h5vcc/h5vcc_runtime.cc +++ b/cobalt/h5vcc/h5vcc_runtime.cc @@ -24,7 +24,8 @@ namespace cobalt { namespace h5vcc { H5vccRuntime::H5vccRuntime(base::EventDispatcher* event_dispatcher) : event_dispatcher_(event_dispatcher), - message_loop_(base::MessageLoop::current()) { + task_runner_(base::ThreadTaskRunnerHandle::Get()) { + DCHECK(task_runner_); on_deep_link_ = new H5vccDeepLinkEventTarget( base::Bind(&H5vccRuntime::GetUnconsumedDeepLink, base::Unretained(this))); on_pause_ = new H5vccRuntimeEventTarget; @@ -87,8 +88,9 @@ void H5vccRuntime::TraceMembers(script::Tracer* tracer) { void H5vccRuntime::OnEventForDeepLink(const base::Event* event) { std::unique_ptr deep_link_event( new base::DeepLinkEvent(event)); - if (base::MessageLoop::current() != message_loop_) { - message_loop_->task_runner()->PostTask( + if (!task_runner_) return; + if (!task_runner_->RunsTasksInCurrentSequence()) { + task_runner_->PostTask( FROM_HERE, base::Bind(&H5vccRuntime::OnDeepLinkEvent, base::Unretained(this), base::Passed(std::move(deep_link_event)))); diff --git a/cobalt/h5vcc/h5vcc_runtime.h b/cobalt/h5vcc/h5vcc_runtime.h index 560543ff08c4..6c0ebc194922 100644 --- a/cobalt/h5vcc/h5vcc_runtime.h +++ b/cobalt/h5vcc/h5vcc_runtime.h @@ -19,6 +19,7 @@ #include #include "base/callback.h" +#include "base/single_thread_task_runner.h" #include "base/threading/thread_checker.h" #include "cobalt/base/deep_link_event.h" #include "cobalt/base/event_dispatcher.h" @@ -63,9 +64,9 @@ class H5vccRuntime : public script::Wrappable { base::EventCallback deep_link_event_callback_; base::OnceClosure consumed_callback_; - // Track the message loop that created this object so deep link events are - // handled from the same thread. - base::MessageLoop* message_loop_; + // Track the task runner from where this object is created so deep link events + // are handled from the same task runner. + scoped_refptr task_runner_; // Thread checker ensures all calls to DOM element are made from the same // thread that it is created in. From ded011ea88d87ec52ad524d395bfa8fd643893e4 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:48:12 -0700 Subject: [PATCH 028/140] Cherry pick PR #1409: Add ability to trigger a timed trace started right before page load. (#1414) Refer to the original PR: https://github.com/youtube/cobalt/pull/1409 This adds a console command 'navigate_timed_trace' that will cause a timed trace to start in `BrowserModule::Navigate()` right after the previous WebModule is destroyed. This allows a 'Reload + Trace' measurement. Also added corresponding buttons to the Cobalt tab in devtools. b/297410701 Co-authored-by: Jelle Foks --- cobalt/browser/browser_module.cc | 32 +++++++++++++++++++ cobalt/browser/browser_module.h | 9 ++++++ .../devtools/front_end/cobalt/cobalt.js | 11 +++++++ 3 files changed, 52 insertions(+) diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc index 5a4c84669926..45b014902073 100644 --- a/cobalt/browser/browser_module.cc +++ b/cobalt/browser/browser_module.cc @@ -52,6 +52,7 @@ #include "cobalt/math/matrix3_f.h" #include "cobalt/overlay_info/overlay_info_registry.h" #include "cobalt/persistent_storage/persistent_settings.h" +#include "cobalt/trace_event/scoped_trace_to_file.h" #include "cobalt/ui_navigation/scroll_engine/scroll_engine.h" #include "cobalt/web/csp_delegate_factory.h" #include "cobalt/web/navigator_ua_data.h" @@ -161,6 +162,13 @@ const char kDisableMediaCodecsCommandLongHelp[] = "is useful when trying to target testing to certain codecs, since other " "codecs will get picked as a fallback as a result."; +const char kNavigateTimedTrace[] = "navigate_timed_trace"; +const char kNavigateTimedTraceShortHelp[] = + "Request a timed trace from the next navigation."; +const char kNavigateTimedTraceLongHelp[] = + "When this is called, a timed trace will start at the next navigation " + "and run for the given number of seconds."; + void ScreenshotCompleteCallback(const base::FilePath& output_path) { DLOG(INFO) << "Screenshot written to " << output_path.value(); } @@ -268,6 +276,11 @@ BrowserModule::BrowserModule(const GURL& url, base::Unretained(this)), kDisableMediaCodecsCommandShortHelp, kDisableMediaCodecsCommandLongHelp)), + ALLOW_THIS_IN_INITIALIZER_LIST(navigate_timed_trace_command_handler_( + kNavigateTimedTrace, + base::Bind(&BrowserModule::OnNavigateTimedTrace, + base::Unretained(this)), + kNavigateTimedTraceShortHelp, kNavigateTimedTraceLongHelp)), #endif // defined(ENABLE_DEBUGGER) has_resumed_(base::WaitableEvent::ResetPolicy::MANUAL, base::WaitableEvent::InitialState::NOT_SIGNALED), @@ -480,6 +493,7 @@ void BrowserModule::Navigate(const GURL& url_reference) { DLOG(INFO) << "In BrowserModule::Navigate " << url; TRACE_EVENT1("cobalt::browser", "BrowserModule::Navigate()", "url", url.spec()); + // Reset the waitable event regardless of the thread. This ensures that the // webdriver won't incorrectly believe that the webmodule has finished loading // when it calls Navigate() and waits for the |web_module_loaded_| signal. @@ -554,6 +568,17 @@ void BrowserModule::NavigateResetWebModule() { current_main_web_module_timeline_id_ = next_timeline_id_++; main_web_module_layer_->Reset(); + +#if defined(ENABLE_DEBUGGER) + // Check to see if a timed_trace has been set, indicating that we should + // begin a timed trace upon startup. + if (navigate_timed_trace_duration_ != base::TimeDelta()) { + trace_event::TraceToFileForDuration( + base::FilePath(FILE_PATH_LITERAL("timed_trace.json")), + navigate_timed_trace_duration_); + navigate_timed_trace_duration_ = base::TimeDelta(); + } +#endif // defined(ENABLE_DEBUGGER) } void BrowserModule::NavigateResetErrorHandling() { @@ -1141,6 +1166,13 @@ void BrowserModule::OnDebugConsoleRenderTreeProduced( SubmitCurrentRenderTreeToRenderer(); } +void BrowserModule::OnNavigateTimedTrace(const std::string& time) { + double duration_in_seconds = 0; + base::StringToDouble(time, &duration_in_seconds); + navigate_timed_trace_duration_ = + base::TimeDelta::FromMilliseconds(static_cast( + duration_in_seconds * base::Time::kMillisecondsPerSecond)); +} #endif // defined(ENABLE_DEBUGGER) void BrowserModule::OnOnScreenKeyboardInputEventProduced( diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h index ab462ad9b8fa..a2d24a2156dc 100644 --- a/cobalt/browser/browser_module.h +++ b/cobalt/browser/browser_module.h @@ -25,6 +25,7 @@ #include "base/synchronization/lock.h" #include "base/synchronization/waitable_event.h" #include "base/threading/thread.h" +#include "base/time/time.h" #include "base/timer/timer.h" #include "cobalt/base/accessibility_caption_settings_changed_event.h" #include "cobalt/base/application_state.h" @@ -385,6 +386,8 @@ class BrowserModule { const browser::WebModule::LayoutResults& layout_results); void OnDebugConsoleRenderTreeProduced( const browser::WebModule::LayoutResults& layout_results); + + void OnNavigateTimedTrace(const std::string& time); #endif // defined(ENABLE_DEBUGGER) #if defined(ENABLE_WEBDRIVER) @@ -660,6 +663,12 @@ class BrowserModule { // Saves the previous debugger state to be restored in the new WebModule. std::unique_ptr debugger_state_; + + // Amount of time to run a Timed Trace after Navigate + base::TimeDelta navigate_timed_trace_duration_; + + debug::console::ConsoleCommandManager::CommandHandler + navigate_timed_trace_command_handler_; #endif // defined(ENABLE_DEBUGGER) // The splash screen. The pointer wrapped here should be non-NULL iff diff --git a/third_party/devtools/front_end/cobalt/cobalt.js b/third_party/devtools/front_end/cobalt/cobalt.js index c623f69fd2d4..40fb60ae4148 100644 --- a/third_party/devtools/front_end/cobalt/cobalt.js +++ b/third_party/devtools/front_end/cobalt/cobalt.js @@ -9,6 +9,7 @@ export default class CobaltPanel extends UI.VBox { ['Trace', 'console_trace.json'], ['Timed Trace', 'timed_trace.json'] ]; + const timed_trace_durations = ['5', '10', '20', '60']; super(true, false); SDK.targetManager.observeTargets(this); @@ -33,6 +34,16 @@ export default class CobaltPanel extends UI.VBox { this.run(`(function() { window.h5vcc.traceEvent.stop();})()`); console.log("Stopped Trace"); })); + traceContainer.appendChild(UI.createLabel('Navigate Timed Trace:')); + timed_trace_durations.forEach((duration) => { + traceContainer.appendChild(UI.createTextButton(Common.UIString(duration + 's'), event => { + console.log("Request Navigate Timed Trace. " + duration); + this._cobaltAgent.invoke_sendConsoleCommand({ + command: 'navigate_timed_trace', message: duration + }); + console.log("Requested Navigate Timed Trace."); + })); + }); trace_files.forEach((file) => { traceContainer.appendChild(UI.createTextButton(Common.UIString('Download ' + file[0]), event => { console.log("Download Trace"); From 9c73f521850a3537c9abb60820b16eb9e8395564 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:49:05 -0700 Subject: [PATCH 029/140] Cherry pick PR #1576: [Android] Improvements around SendAndroidCommand to avoid ANRs. (#1583) Refer to the original PR: https://github.com/youtube/cobalt/pull/1576 This makes |g_app_running| an atomic (with barriers), and ensures it's set to false before destroying ApplicationAndroid. This should avoid dereferencing |ApplicationAndroid| during application shutdown, avoiding the corresponding crashes and ANRs. b/225209442 Co-authored-by: Jelle Foks --- starboard/android/shared/android_main.cc | 39 ++++++++++++++---------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/starboard/android/shared/android_main.cc b/starboard/android/shared/android_main.cc index 4477656ccdef..7bccf5080083 100644 --- a/starboard/android/shared/android_main.cc +++ b/starboard/android/shared/android_main.cc @@ -17,6 +17,7 @@ #include "starboard/android/shared/jni_env_ext.h" #include "starboard/android/shared/jni_utils.h" #include "starboard/android/shared/log_internal.h" +#include "starboard/common/atomic.h" #include "starboard/common/file.h" #include "starboard/common/semaphore.h" #include "starboard/common/string.h" @@ -48,7 +49,7 @@ Semaphore* g_app_created_semaphore = nullptr; // Safeguard to avoid sending AndroidCommands either when there is no instance // of the Starboard application, or after the run loop has exited and the // ALooper receiving the commands is no longer being polled. -bool g_app_running = false; +atomic_bool g_app_running; std::vector GetArgs() { std::vector args; @@ -235,7 +236,7 @@ void* ThreadEntryPoint(void* context) { // Mark the app running before signaling app created so there's no race to // allow sending the first AndroidCommand after onCreate() returns. - g_app_running = true; + g_app_running.store(true); // Signal GameActivity_onCreate() that it may proceed. g_app_created_semaphore->Put(); @@ -243,12 +244,12 @@ void* ThreadEntryPoint(void* context) { // Enter the Starboard run loop until stopped. int error_level = app.Run(std::move(command_line), GetStartDeepLink().c_str()); -#endif // SB_API_VERSION >= 15 // Mark the app not running before informing StarboardBridge that the app is // stopped so that we won't send any more AndroidCommands as a result of // shutting down the Activity. - g_app_running = false; + g_app_running.store(false); +#endif // SB_API_VERSION >= 15 // Our launcher.py looks for this to know when the app (test) is done. SB_LOG(INFO) << "***Application Stopped*** " << error_level; @@ -262,13 +263,13 @@ void* ThreadEntryPoint(void* context) { } void OnStart(GameActivity* activity) { - if (g_app_running) { + if (g_app_running.load()) { ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStart); } } void OnResume(GameActivity* activity) { - if (g_app_running) { + if (g_app_running.load()) { // Stop the MediaPlaybackService if activity state transits from background // to foreground. Note that the MediaPlaybackService may already have // been stopped before Cobalt's lifecycle state transits from Concealed @@ -279,7 +280,7 @@ void OnResume(GameActivity* activity) { } void OnPause(GameActivity* activity) { - if (g_app_running) { + if (g_app_running.load()) { // Start the MediaPlaybackService before activity state transits from // foreground to background. ApplicationAndroid::Get()->StartMediaPlaybackService(); @@ -288,28 +289,28 @@ void OnPause(GameActivity* activity) { } void OnStop(GameActivity* activity) { - if (g_app_running) { + if (g_app_running.load()) { ApplicationAndroid::Get()->SendAndroidCommand(AndroidCommand::kStop); } } bool OnTouchEvent(GameActivity* activity, const GameActivityMotionEvent* event) { - if (g_app_running) { + if (g_app_running.load()) { return ApplicationAndroid::Get()->SendAndroidMotionEvent(event); } return false; } bool OnKey(GameActivity* activity, const GameActivityKeyEvent* event) { - if (g_app_running) { + if (g_app_running.load()) { return ApplicationAndroid::Get()->SendAndroidKeyEvent(event); } return false; } void OnWindowFocusChanged(GameActivity* activity, bool focused) { - if (g_app_running) { + if (g_app_running.load()) { ApplicationAndroid::Get()->SendAndroidCommand( focused ? AndroidCommand::kWindowFocusGained : AndroidCommand::kWindowFocusLost); @@ -317,14 +318,14 @@ void OnWindowFocusChanged(GameActivity* activity, bool focused) { } void OnNativeWindowCreated(GameActivity* activity, ANativeWindow* window) { - if (g_app_running) { + if (g_app_running.load()) { ApplicationAndroid::Get()->SendAndroidCommand( AndroidCommand::kNativeWindowCreated, window); } } void OnNativeWindowDestroyed(GameActivity* activity, ANativeWindow* window) { - if (g_app_running) { + if (g_app_running.load()) { ApplicationAndroid::Get()->SendAndroidCommand( AndroidCommand::kNativeWindowDestroyed); } @@ -380,7 +381,7 @@ extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_coat_VolumeStateReceiver_nativeVolumeChanged(JNIEnv* env, jobject jcaller, jint volumeDelta) { - if (g_app_running) { + if (g_app_running.load()) { SbKey key = volumeDelta > 0 ? SbKey::kSbKeyVolumeUp : SbKey::kSbKeyVolumeDown; ApplicationAndroid::Get()->SendKeyboardInject(key); @@ -390,7 +391,7 @@ Java_dev_cobalt_coat_VolumeStateReceiver_nativeVolumeChanged(JNIEnv* env, extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_coat_VolumeStateReceiver_nativeMuteChanged(JNIEnv* env, jobject jcaller) { - if (g_app_running) { + if (g_app_running.load()) { ApplicationAndroid::Get()->SendKeyboardInject(SbKey::kSbKeyVolumeMute); } } @@ -413,7 +414,7 @@ extern "C" int SbRunStarboardMain(int argc, // Mark the app running before signaling app created so there's no race to // allow sending the first AndroidCommand after onCreate() returns. - g_app_running = true; + g_app_running.store(true); // Signal GameActivity_onCreate() that it may proceed. g_app_created_semaphore->Put(); @@ -421,6 +422,12 @@ extern "C" int SbRunStarboardMain(int argc, // Enter the Starboard run loop until stopped. int error_level = app.Run(std::move(command_line), GetStartDeepLink().c_str()); + + // Mark the app not running before informing StarboardBridge that the app is + // stopped so that we won't send any more AndroidCommands as a result of + // shutting down the Activity. + g_app_running.store(false); + return error_level; } #endif // SB_API_VERSION >= 15 From 50301fc178815a29f026743195858336e64a833a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:00:28 -0700 Subject: [PATCH 030/140] Cherry pick PR #1454: Fix service worker ShouldSkipEvent() crash (#1483) Refer to the original PR: https://github.com/youtube/cobalt/pull/1454 Based on the error trace, at this step, installing_worker can be a null pointer. Not sure how to repro this edge case, but in the blow code, there are also places to check if installing_worker() is null. Probably we should check it before call ShouldSkipEvent(). b/298063403 Co-authored-by: Sherry Zhou --- cobalt/worker/service_worker_jobs.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cobalt/worker/service_worker_jobs.cc b/cobalt/worker/service_worker_jobs.cc index fa0451b16096..ecbb12653237 100644 --- a/cobalt/worker/service_worker_jobs.cc +++ b/cobalt/worker/service_worker_jobs.cc @@ -852,7 +852,8 @@ void ServiceWorkerJobs::Install( ServiceWorkerObject* installing_worker = registration->installing_worker(); // 11. If the result of running the Should Skip Event algorithm with // installingWorker and "install" is false, then: - if (!installing_worker->ShouldSkipEvent(base::Tokens::install())) { + if (installing_worker && + !installing_worker->ShouldSkipEvent(base::Tokens::install())) { // 11.1. Let forceBypassCache be true if job’s force bypass cache flag is // set, and false otherwise. bool force_bypass_cache = job->force_bypass_cache_flag; From 5b2c769a42cef164ecf0319aff6eb6c6926e3a56 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:00:53 -0700 Subject: [PATCH 031/140] Cherry pick PR #1545: Remove Service worker persistent setting origin url check (#1548) Refer to the original PR: https://github.com/youtube/cobalt/pull/1545 When register a service worker, we never check anything about the initial url. However, in getRegistration(), it queries service worker persistent settings, which compares the origin with the initial url. As a result, we can register a service worker, but never get them from persistent setting file. And those service workers will not be unregistered as well. In addition to the fix here, we probably needs to have a better way to prevent the Service worker persistent setting file to grow infinitely. b/300293281 Co-authored-by: Sherry Zhou --- cobalt/browser/browser_module.cc | 2 +- cobalt/browser/service_worker_registry.cc | 8 ++++---- cobalt/browser/service_worker_registry.h | 5 ++--- cobalt/layout_tests/web_platform_tests.cc | 2 +- cobalt/worker/service_worker_context.cc | 5 ++--- cobalt/worker/service_worker_context.h | 2 +- cobalt/worker/service_worker_persistent_settings.cc | 6 ------ cobalt/worker/service_worker_persistent_settings.h | 6 ++---- 8 files changed, 13 insertions(+), 23 deletions(-) diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc index 45b014902073..85d5d93680c6 100644 --- a/cobalt/browser/browser_module.cc +++ b/cobalt/browser/browser_module.cc @@ -302,7 +302,7 @@ BrowserModule::BrowserModule(const GURL& url, platform_info_.reset(new browser::UserAgentPlatformInfo()); service_worker_registry_.reset(new ServiceWorkerRegistry( - &web_settings_, network_module, platform_info_.get(), url)); + &web_settings_, network_module, platform_info_.get())); #if SB_HAS(CORE_DUMP_HANDLER_SUPPORT) SbCoreDumpRegisterHandler(BrowserModule::CoreDumpHandler, this); diff --git a/cobalt/browser/service_worker_registry.cc b/cobalt/browser/service_worker_registry.cc index 89b9395b156b..2a62dac3930d 100644 --- a/cobalt/browser/service_worker_registry.cc +++ b/cobalt/browser/service_worker_registry.cc @@ -49,7 +49,7 @@ void ServiceWorkerRegistry::WillDestroyCurrentMessageLoop() { ServiceWorkerRegistry::ServiceWorkerRegistry( web::WebSettings* web_settings, network::NetworkModule* network_module, - web::UserAgentPlatformInfo* platform_info, const GURL& url) + web::UserAgentPlatformInfo* platform_info) : thread_("ServiceWorkerRegistry") { if (!thread_.Start()) return; DCHECK(message_loop()); @@ -73,7 +73,7 @@ ServiceWorkerRegistry::ServiceWorkerRegistry( message_loop()->task_runner()->PostTask( FROM_HERE, base::Bind(&ServiceWorkerRegistry::Initialize, base::Unretained(this), - web_settings, network_module, platform_info, url)); + web_settings, network_module, platform_info)); // Register as a destruction observer to shut down the Web Agent once all // pending tasks have been executed and the message loop is about to be @@ -145,11 +145,11 @@ worker::ServiceWorkerContext* ServiceWorkerRegistry::service_worker_context() { void ServiceWorkerRegistry::Initialize( web::WebSettings* web_settings, network::NetworkModule* network_module, - web::UserAgentPlatformInfo* platform_info, const GURL& url) { + web::UserAgentPlatformInfo* platform_info) { TRACE_EVENT0("cobalt::browser", "ServiceWorkerRegistry::Initialize()"); DCHECK_EQ(base::MessageLoop::current(), message_loop()); service_worker_context_.reset(new worker::ServiceWorkerContext( - web_settings, network_module, platform_info, message_loop(), url)); + web_settings, network_module, platform_info, message_loop())); } } // namespace browser diff --git a/cobalt/browser/service_worker_registry.h b/cobalt/browser/service_worker_registry.h index f0e934ee6578..de31979b36a3 100644 --- a/cobalt/browser/service_worker_registry.h +++ b/cobalt/browser/service_worker_registry.h @@ -35,8 +35,7 @@ class ServiceWorkerRegistry : public base::MessageLoop::DestructionObserver { public: ServiceWorkerRegistry(web::WebSettings* web_settings, network::NetworkModule* network_module, - web::UserAgentPlatformInfo* platform_info, - const GURL& url); + web::UserAgentPlatformInfo* platform_info); ~ServiceWorkerRegistry(); // The message loop this object is running on. @@ -58,7 +57,7 @@ class ServiceWorkerRegistry : public base::MessageLoop::DestructionObserver { // the dedicated thread. void Initialize(web::WebSettings* web_settings, network::NetworkModule* network_module, - web::UserAgentPlatformInfo* platform_info, const GURL& url); + web::UserAgentPlatformInfo* platform_info); void PingWatchdog(); diff --git a/cobalt/layout_tests/web_platform_tests.cc b/cobalt/layout_tests/web_platform_tests.cc index 455c37eeabae..01039d374879 100644 --- a/cobalt/layout_tests/web_platform_tests.cc +++ b/cobalt/layout_tests/web_platform_tests.cc @@ -219,7 +219,7 @@ std::string RunWebPlatformTest(const GURL& url, bool* got_results) { new browser::UserAgentPlatformInfo()); std::unique_ptr service_worker_registry( new browser::ServiceWorkerRegistry(&web_settings, &network_module, - platform_info.get(), url)); + platform_info.get())); browser::WebModule::Options web_module_options; // Use test runner mode to allow the content itself to dictate when it is diff --git a/cobalt/worker/service_worker_context.cc b/cobalt/worker/service_worker_context.cc index b4b4bac2d11b..ad96c62c2e3b 100644 --- a/cobalt/worker/service_worker_context.cc +++ b/cobalt/worker/service_worker_context.cc @@ -163,15 +163,14 @@ void ResolveGetClientPromise( ServiceWorkerContext::ServiceWorkerContext( web::WebSettings* web_settings, network::NetworkModule* network_module, - web::UserAgentPlatformInfo* platform_info, base::MessageLoop* message_loop, - const GURL& url) + web::UserAgentPlatformInfo* platform_info, base::MessageLoop* message_loop) : message_loop_(message_loop) { DCHECK_EQ(message_loop_, base::MessageLoop::current()); jobs_ = std::make_unique(this, network_module, message_loop); ServiceWorkerPersistentSettings::Options options(web_settings, network_module, - platform_info, this, url); + platform_info, this); scope_to_registration_map_.reset(new ServiceWorkerRegistrationMap(options)); DCHECK(scope_to_registration_map_); } diff --git a/cobalt/worker/service_worker_context.h b/cobalt/worker/service_worker_context.h index e51882d6270b..460200fb5a89 100644 --- a/cobalt/worker/service_worker_context.h +++ b/cobalt/worker/service_worker_context.h @@ -51,7 +51,7 @@ class ServiceWorkerContext { ServiceWorkerContext(web::WebSettings* web_settings, network::NetworkModule* network_module, web::UserAgentPlatformInfo* platform_info, - base::MessageLoop* message_loop, const GURL& url); + base::MessageLoop* message_loop); ~ServiceWorkerContext(); base::MessageLoop* message_loop() { return message_loop_; } diff --git a/cobalt/worker/service_worker_persistent_settings.cc b/cobalt/worker/service_worker_persistent_settings.cc index fc81821f4e3f..368ba1a631e3 100644 --- a/cobalt/worker/service_worker_persistent_settings.cc +++ b/cobalt/worker/service_worker_persistent_settings.cc @@ -119,12 +119,6 @@ void ServiceWorkerPersistentSettings::ReadServiceWorkerRegistrationMapSettings( url::Origin storage_key = url::Origin::Create(GURL(dict[kSettingsStorageKeyKey]->GetString())); - // Only add persisted workers to the registration_map - // if their storage_key matches the origin of the initial_url. - if (!storage_key.IsSameOriginWith(url::Origin::Create(options_.url))) { - continue; - } - if (!CheckPersistentValue(key_string, kSettingsScopeUrlKey, dict, base::Value::Type::STRING)) continue; diff --git a/cobalt/worker/service_worker_persistent_settings.h b/cobalt/worker/service_worker_persistent_settings.h index f8558954782d..55142a12c16c 100644 --- a/cobalt/worker/service_worker_persistent_settings.h +++ b/cobalt/worker/service_worker_persistent_settings.h @@ -48,17 +48,15 @@ class ServiceWorkerPersistentSettings { Options(web::WebSettings* web_settings, network::NetworkModule* network_module, web::UserAgentPlatformInfo* platform_info, - ServiceWorkerContext* service_worker_context, const GURL& url) + ServiceWorkerContext* service_worker_context) : web_settings(web_settings), network_module(network_module), platform_info(platform_info), - service_worker_context(service_worker_context), - url(url) {} + service_worker_context(service_worker_context) {} web::WebSettings* web_settings; network::NetworkModule* network_module; web::UserAgentPlatformInfo* platform_info; ServiceWorkerContext* service_worker_context; - const GURL& url; }; explicit ServiceWorkerPersistentSettings(const Options& options); From 4b858e88c90c7c0fae70b909b2943d7bc05197d8 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:02:44 -0700 Subject: [PATCH 032/140] Cherry pick PR #1564: Cherry pick wasm deadlock fix from upstream. (#1575) Refer to the original PR: https://github.com/youtube/cobalt/pull/1564 This cherry picks https://crrev.com/c/2652488 to fix an ANR triggered when a SCREEN_OFF intent is received. This won't address all ANRs from that event, but one that is common enough to be spotted in the cluster. b/300502599 b/300521831 Co-authored-by: Jelle Foks --- third_party/v8/src/wasm/wasm-code-manager.cc | 12 ++++++++++-- third_party/v8/src/wasm/wasm-code-manager.h | 8 ++++++++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/third_party/v8/src/wasm/wasm-code-manager.cc b/third_party/v8/src/wasm/wasm-code-manager.cc index 579c09429f7e..fd83a600ff61 100644 --- a/third_party/v8/src/wasm/wasm-code-manager.cc +++ b/third_party/v8/src/wasm/wasm-code-manager.cc @@ -1123,6 +1123,10 @@ WasmCode* NativeModule::PublishCodeLocked(std::unique_ptr code) { // The caller must hold the {allocation_mutex_}, thus we fail to lock it here. DCHECK(!allocation_mutex_.TryLock()); + // Add the code to the surrounding code ref scope, so the returned pointer is + // guaranteed to be valid. + WasmCodeRefScope::AddRef(code.get()); + if (!code->IsAnonymous() && code->index() >= module_->num_imported_functions) { DCHECK_LT(code->index(), num_functions()); @@ -1159,17 +1163,21 @@ WasmCode* NativeModule::PublishCodeLocked(std::unique_ptr code) { WasmCodeRefScope::AddRef(prior_code); // The code is added to the current {WasmCodeRefScope}, hence the ref // count cannot drop to zero here. - CHECK(!prior_code->DecRef()); + prior_code->DecRefOnLiveCode(); } PatchJumpTablesLocked(slot_idx, code->instruction_start()); + } else { + // The code tables does not hold a reference to the code, hence decrement + // the initial ref count of 1. The code was added to the + // {WasmCodeRefScope} though, so it cannot die here. + code->DecRefOnLiveCode(); } if (!code->for_debugging() && tiering_state_ == kTieredDown && code->tier() == ExecutionTier::kTurbofan) { liftoff_bailout_count_.fetch_add(1); } } - WasmCodeRefScope::AddRef(code.get()); WasmCode* result = code.get(); owned_code_.emplace(result->instruction_start(), std::move(code)); return result; diff --git a/third_party/v8/src/wasm/wasm-code-manager.h b/third_party/v8/src/wasm/wasm-code-manager.h index 26a9030d4efb..da8e2ccdc1af 100644 --- a/third_party/v8/src/wasm/wasm-code-manager.h +++ b/third_party/v8/src/wasm/wasm-code-manager.h @@ -226,6 +226,14 @@ class V8_EXPORT_PRIVATE WasmCode final { } } + // Decrement the ref count on code that is known to be in use (i.e. the ref + // count cannot drop to zero here). + void DecRefOnLiveCode() { + int old_count = ref_count_.fetch_sub(1, std::memory_order_acq_rel); + DCHECK_LE(2, old_count); + USE(old_count); + } + // Decrement the ref count on code that is known to be dead, even though there // might still be C++ references. Returns whether this drops the last // reference and the code needs to be freed. From eb925e42c587903e7805a3bfe9a7ec86b000045f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 10:30:27 -0700 Subject: [PATCH 033/140] Cherry pick PR #1453: [Android] Don't release the lock to clear the video surface. (#1460) Refer to the original PR: https://github.com/youtube/cobalt/pull/1453 To ensure that resetting the surface doesn't race with other accesses to the surface, keep the lock the entire time. During this, replace the SurfaceHolderCallback with explicit and synchronous equivalent behavior. b/298213425 b/297264187 Co-authored-by: Jelle Foks --- .../dev/cobalt/media/VideoSurfaceView.java | 33 +++++++++++---- starboard/android/shared/video_window.cc | 41 +++++++++++-------- 2 files changed, 50 insertions(+), 24 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java index 339e36365453..816c6495af14 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java @@ -37,6 +37,7 @@ public class VideoSurfaceView extends SurfaceView { private static Surface currentSurface = null; + private SurfaceHolder.Callback mSurfaceHolderCallback = null; private static final Set needResetSurfaceList = new HashSet<>(); @@ -71,7 +72,8 @@ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, i private void initialize(Context context) { setBackgroundColor(Color.TRANSPARENT); - getHolder().addCallback(new SurfaceHolderCallback()); + mSurfaceHolderCallback = new SurfaceHolderCallback(); + getHolder().addCallback(mSurfaceHolderCallback); // TODO: Avoid recreating the surface when the player bounds change. // Recreating the surface is time-consuming and complicates synchronizing @@ -79,20 +81,37 @@ private void initialize(Context context) { } public void clearSurface() { - if (getHolder().getSurface().isValid()) { - Canvas canvas = getHolder().lockCanvas(); + SurfaceHolder holder = getHolder(); + if (holder == null) { + return; + } + Surface surface = holder.getSurface(); + if ((surface != null) && surface.isValid()) { + Canvas canvas = holder.lockCanvas(); if (canvas != null) { canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); - getHolder().unlockCanvasAndPost(canvas); + holder.unlockCanvasAndPost(canvas); } - // Trigger a surface changed event to prevent 'already connected'. - getHolder().setFormat(PixelFormat.TRANSPARENT); - getHolder().setFormat(PixelFormat.OPAQUE); + } + // Trigger a surface changed event to prevent 'already connected'. + // But disable the callback to prevent it from making calls to the locking + // nativeOnVideoSurfaceChanged because we already are holding the same lock. + if (mSurfaceHolderCallback != null) { + holder.removeCallback(mSurfaceHolderCallback); + } + holder.setFormat(PixelFormat.TRANSPARENT); + holder.setFormat(PixelFormat.OPAQUE); + currentSurface = holder.getSurface(); + nativeOnVideoSurfaceChangedLocked(currentSurface); + if (mSurfaceHolderCallback != null) { + holder.addCallback(mSurfaceHolderCallback); } } private static native void nativeOnVideoSurfaceChanged(Surface surface); + private static native void nativeOnVideoSurfaceChangedLocked(Surface surface); + private static native void nativeSetNeedResetSurface(); private class SurfaceHolderCallback implements SurfaceHolder.Callback { diff --git a/starboard/android/shared/video_window.cc b/starboard/android/shared/video_window.cc index 15237ec6164d..a47cd54c1854 100644 --- a/starboard/android/shared/video_window.cc +++ b/starboard/android/shared/video_window.cc @@ -47,11 +47,10 @@ bool g_reset_surface_on_clear_window = false; } // namespace extern "C" SB_EXPORT_PLATFORM void -Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged( +Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChangedLocked( JNIEnv* env, jobject unused_this, jobject surface) { - ScopedLock lock(*GetViewSurfaceMutex()); if (g_video_surface_holder) { g_video_surface_holder->OnSurfaceDestroyed(); g_video_surface_holder = NULL; @@ -70,6 +69,16 @@ Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged( } } +extern "C" SB_EXPORT_PLATFORM void +Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged( + JNIEnv* env, + jobject j_this, + jobject surface) { + ScopedLock lock(*GetViewSurfaceMutex()); + Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChangedLocked( + env, j_this, surface); +} + extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_media_VideoSurfaceView_nativeSetNeedResetSurface( JNIEnv* env, @@ -119,26 +128,24 @@ bool VideoSurfaceHolder::GetVideoWindowSize(int* width, int* height) { void VideoSurfaceHolder::ClearVideoWindow(bool force_reset_surface) { // Lock *GetViewSurfaceMutex() here, to avoid releasing g_native_video_window // during painting. - { - ScopedLock lock(*GetViewSurfaceMutex()); + ScopedLock lock(*GetViewSurfaceMutex()); - if (!g_native_video_window) { - SB_LOG(INFO) << "Tried to clear video window when it was null."; - return; - } + if (!g_native_video_window) { + SB_LOG(INFO) << "Tried to clear video window when it was null."; + return; + } - if (force_reset_surface) { + if (force_reset_surface) { + JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", + "()V"); + return; + } else if (g_reset_surface_on_clear_window) { + int width = ANativeWindow_getWidth(g_native_video_window); + int height = ANativeWindow_getHeight(g_native_video_window); + if (width <= height) { JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", "()V"); return; - } else if (g_reset_surface_on_clear_window) { - int width = ANativeWindow_getWidth(g_native_video_window); - int height = ANativeWindow_getHeight(g_native_video_window); - if (width <= height) { - JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", - "()V"); - return; - } } } JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("clearVideoSurface", "()V"); From 8c3b81377247954bef49a17771bc9a211a55bc26 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:19:13 -0700 Subject: [PATCH 034/140] Cherry pick PR #1488: Use SbThreadSleep for cval_stats_test. (#1496) Refer to the original PR: https://github.com/youtube/cobalt/pull/1488 This should fix test on Evergreen. b/292030213 Change-Id: I4514aa08729fa814f58a3f2cf1b7b0339e59574d Co-authored-by: thorsten sideb0ard --- cobalt/build/cobalt_configuration.py | 3 +-- cobalt/media/base/cval_stats_test.cc | 29 ++++++++-------------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/cobalt/build/cobalt_configuration.py b/cobalt/build/cobalt_configuration.py index e1320ab077c1..ca1c943ea270 100644 --- a/cobalt/build/cobalt_configuration.py +++ b/cobalt/build/cobalt_configuration.py @@ -133,8 +133,7 @@ def GetTestTargets(self): 'media_capture_test', 'media_session_test', 'media_stream_test', - # TODO(b/292030213): Crashes on evergreen - # 'media_test', + 'media_test', 'memory_store_test', 'metrics_test', 'nb_test', diff --git a/cobalt/media/base/cval_stats_test.cc b/cobalt/media/base/cval_stats_test.cc index 673e20cc477f..ceba32024d9a 100644 --- a/cobalt/media/base/cval_stats_test.cc +++ b/cobalt/media/base/cval_stats_test.cc @@ -12,16 +12,12 @@ // See the License for the specific language governing permissions and // limitations under the License. -#ifdef _WIN32 -#include -#else -#include -#endif +#include "cobalt/media/base/cval_stats.h" #include #include -#include "cobalt/media/base/cval_stats.h" +#include "starboard/thread.h" #include "testing/gtest/include/gtest/gtest.h" @@ -33,21 +29,11 @@ const char kCValnameMinimum[] = "Media.SbPlayerCreateTime.Minimum"; const char kPipelineIdentifier[] = "test_pipeline"; -constexpr int kSleepTimeMs = 50; +constexpr SbTime kSleepTime = 50; // 50 microseconds namespace cobalt { namespace media { -namespace { -void sleep_ms(int ms) { -#ifdef _WIN32 - Sleep(ms); -#else - usleep(ms); -#endif -} -} // namespace - TEST(MediaCValStatsTest, InitiallyEmpty) { base::CValManager* cvm = base::CValManager::GetInstance(); EXPECT_TRUE(cvm); @@ -71,7 +57,8 @@ TEST(MediaCValStatsTest, NothingRecorded) { CValStats cval_stats_; cval_stats_.StartTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); - sleep_ms(kSleepTimeMs); + SbThreadSleep(kSleepTime); + cval_stats_.StopTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); base::Optional result = @@ -88,7 +75,7 @@ TEST(MediaCValStatsTest, EnableRecording) { cval_stats_.Enable(true); cval_stats_.StartTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); - sleep_ms(kSleepTimeMs); + SbThreadSleep(kSleepTime); cval_stats_.StopTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); base::Optional result = @@ -114,7 +101,7 @@ TEST(MediaCValStatsTest, DontGenerateHistoricalData) { for (int i = 0; i < kMediaDefaultMaxSamplesBeforeCalculation - 1; i++) { cval_stats_.StartTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); - sleep_ms(kSleepTimeMs); + SbThreadSleep(kSleepTime); cval_stats_.StopTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); } @@ -141,7 +128,7 @@ TEST(MediaCValStatsTest, GenerateHistoricalData) { for (int i = 0; i < kMediaDefaultMaxSamplesBeforeCalculation; i++) { cval_stats_.StartTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); - sleep_ms(kSleepTimeMs); + SbThreadSleep(kSleepTime); cval_stats_.StopTimer(MediaTiming::SbPlayerCreate, kPipelineIdentifier); } From 1b7aabcdbe3fdfb7fa2e607a21597ec43ba50c56 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:22:34 -0700 Subject: [PATCH 035/140] Cherry pick PR #1433: Generate debug info for non CI builds or qa, gold builds (#1435) Refer to the original PR: https://github.com/youtube/cobalt/pull/1433 b/290953361 Fixes large archives generated for devel, debug builds on CI. Address issues for building pdb's locally: https://github.com/youtube/cobalt/pull/1249#issuecomment-1679395582 Co-authored-by: Niranjan Yardi --- starboard/xb1/platform_configuration/BUILD.gn | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/starboard/xb1/platform_configuration/BUILD.gn b/starboard/xb1/platform_configuration/BUILD.gn index 68b78621c0be..db2a0b280132 100644 --- a/starboard/xb1/platform_configuration/BUILD.gn +++ b/starboard/xb1/platform_configuration/BUILD.gn @@ -59,7 +59,9 @@ config("target") { ] } - ldflags += [ "/DEBUG:FASTLINK" ] + if (is_qa || is_gold || !cobalt_fastbuild) { + ldflags += [ "/DEBUG:FASTLINK" ] + } ldflags += [ "/NODEFAULTLIB" ] arflags += [ "/NODEFAULTLIB" ] From f00f9e930a7d56012f72d24d3fa78405dd2b9a9b Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:35:55 -0700 Subject: [PATCH 036/140] Cherry pick PR #461: [UWP] Use a shared output queue for sw av1 and vpx decoder (#1380) Refer to the original PR: https://github.com/youtube/cobalt/pull/461 Reorganize output queue. Use one large preallocated hw buffer for both av1 & vpx sw decoders b/249739051 Change-Id: I6430ae1ba5d288ed2495f3056cc7283f5c189f49 Co-authored-by: victorpasoshnikov --- .../shared/uwp/extended_resources_manager.cc | 89 ++++- .../shared/uwp/extended_resources_manager.h | 6 +- .../shared/uwp/player_components_factory.cc | 8 +- .../xb1/shared/gpu_base_video_decoder.cc | 309 +++++++++++++++--- starboard/xb1/shared/gpu_base_video_decoder.h | 82 ++++- 5 files changed, 427 insertions(+), 67 deletions(-) diff --git a/starboard/shared/uwp/extended_resources_manager.cc b/starboard/shared/uwp/extended_resources_manager.cc index 5b0c78b3b905..d7a82e013eb6 100644 --- a/starboard/shared/uwp/extended_resources_manager.cc +++ b/starboard/shared/uwp/extended_resources_manager.cc @@ -47,6 +47,29 @@ using ::starboard::xb1::shared::VpxVideoDecoder; const SbTime kReleaseTimeout = kSbTimeSecond; +// kFrameBuffersPoolMemorySize is the size of gpu memory heap for common use +// by vpx & av1 sw decoders. +// This value must be greater then max(av1_min_value, vpx_min_value), where +// av1_min_value & vpx_min_value are minimal required memory size for sw av1 & +// vpx decoders. +// +// Vpx sw decoder needs 13 internal frame buffers for work and at least +// 8 buffers for preroll. +// The size of fb is 13762560 for 4K SDR and 12976128 for 2K HDR +// So, vpx decoder needs minimum 13762560 * (13 + preroll_size) = 289013760 +// bytes. +// +// Av1 sw decoder needs 13 internal buffers and 8 buffers for preroll. +// The size of fb is 5996544 for 2K SDR and 11993088 for 2K HDR +// av1 decoder needs minimum 11993088 * (13 + preroll_size) = 251854848 bytes. +// +// So, the value 289013760 is minimal for reliable decoders working. +// +// To make playback more smooth it is better to increase the output queue size +// up to 30-50 frames, but it should not exceed memory budgetd. +// So, the value of 440 Mb looks as compromise. +const uint64_t kFrameBuffersPoolMemorySize = 440 * 1024 * 1024; + bool IsExtendedResourceModeRequired() { if (!::starboard::xb1::shared::CanAcquire()) { return false; @@ -150,6 +173,7 @@ void ExtendedResourcesManager::Quit() { bool ExtendedResourcesManager::GetD3D12Objects( Microsoft::WRL::ComPtr* device, + Microsoft::WRL::ComPtr* buffer_heap, void** command_queue) { if (HasNonrecoverableFailure()) { SB_LOG(WARNING) << "The D3D12 device has encountered a nonrecoverable " @@ -184,8 +208,8 @@ bool ExtendedResourcesManager::GetD3D12Objects( D3D12_HEAP_PROPERTIES prop = CD3DX12_HEAP_PROPERTIES(D3D12_HEAP_TYPE_DEFAULT); D3D12_RESOURCE_DESC desc = CD3DX12_RESOURCE_DESC::Buffer(1024 * 1024); HRESULT result = d3d12device_->CreateCommittedResource( - &prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COPY_DEST, - nullptr, IID_PPV_ARGS(&res)); + &prop, D3D12_HEAP_FLAG_NONE, &desc, D3D12_RESOURCE_STATE_COMMON, nullptr, + IID_PPV_ARGS(&res)); if (result != S_OK) { SB_LOG(WARNING) << "The D3D12 device is not in a good state, can not use " "GPU based decoders."; @@ -196,11 +220,25 @@ bool ExtendedResourcesManager::GetD3D12Objects( *device = d3d12device_; *command_queue = d3d12queue_.Get(); + *buffer_heap = d3d12FrameBuffersHeap_.Get(); return true; } bool ExtendedResourcesManager::GetD3D12ObjectsInternal() { if (!d3d12device_) { + UINT dxgiFactoryFlags = 0; +#if defined(_DEBUG) + { + // This can help to debug DX issues. If something goes wrong in DX, + // Debug Layer outputs detailed log + ComPtr debugController; + HRESULT hr = D3D12GetDebugInterface(IID_PPV_ARGS(&debugController)); + if (SUCCEEDED(hr)) { + debugController->EnableDebugLayer(); + } + } +#endif + if (FAILED(D3D12CreateDevice(NULL, D3D_FEATURE_LEVEL_11_0, IID_PPV_ARGS(&d3d12device_)))) { // GPU based vp9 decoding will be temporarily disabled. @@ -221,8 +259,26 @@ bool ExtendedResourcesManager::GetD3D12ObjectsInternal() { } SB_DCHECK(d3d12queue_); } + if (!d3d12FrameBuffersHeap_) { + D3D12_HEAP_DESC heap_desc; + heap_desc.SizeInBytes = kFrameBuffersPoolMemorySize; + heap_desc.Properties.Type = D3D12_HEAP_TYPE_DEFAULT; + heap_desc.Properties.CPUPageProperty = D3D12_CPU_PAGE_PROPERTY_UNKNOWN; + heap_desc.Properties.MemoryPoolPreference = D3D12_MEMORY_POOL_UNKNOWN; + heap_desc.Properties.CreationNodeMask = 0; + heap_desc.Properties.VisibleNodeMask = 0; + heap_desc.Alignment = D3D12_DEFAULT_RESOURCE_PLACEMENT_ALIGNMENT; + heap_desc.Flags = D3D12_HEAP_FLAG_NONE; + + if (FAILED(d3d12device_->CreateHeap( + &heap_desc, IID_PPV_ARGS(&d3d12FrameBuffersHeap_)))) { + SB_LOG(WARNING) << "Failed to create d3d12 buffer."; + return false; + } + SB_DCHECK(d3d12FrameBuffersHeap_); + } - return d3d12device_ && d3d12queue_; + return d3d12device_ && d3d12queue_ && d3d12FrameBuffersHeap_; } bool ExtendedResourcesManager::AcquireExtendedResourcesInternal() { @@ -335,7 +391,8 @@ void ExtendedResourcesManager::CompileShadersAsynchronously() { "shader compile."; return; } - if (Av1VideoDecoder::CompileShaders(d3d12device_, d3d12queue_.Get())) { + if (Av1VideoDecoder::CompileShaders(d3d12device_, d3d12FrameBuffersHeap_, + d3d12queue_.Get())) { is_av1_shader_compiled_ = true; SB_LOG(INFO) << "Gpu based AV1 decoder finished compiling its shaders."; } else { @@ -352,7 +409,8 @@ void ExtendedResourcesManager::CompileShadersAsynchronously() { return; } - if (VpxVideoDecoder::CompileShaders(d3d12device_, d3d12queue_.Get())) { + if (VpxVideoDecoder::CompileShaders(d3d12device_, d3d12FrameBuffersHeap_, + d3d12queue_.Get())) { is_vp9_shader_compiled_ = true; SB_LOG(INFO) << "Gpu based VP9 decoder finished compiling its shaders."; } else { @@ -372,10 +430,6 @@ void ExtendedResourcesManager::CompileShadersAsynchronously() { void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { SB_DCHECK(thread_checker_.CalledOnValidThread()); -#if defined(INTERNAL_BUILD) - Av1VideoDecoder::ClearFrameBufferPool(); -#endif // defined(INTERNAL_BUILD) - ScopedLock scoped_lock(mutex_); if (!is_extended_resources_acquired_.load()) { SB_LOG(INFO) << "Extended resources hasn't been acquired," @@ -424,8 +478,7 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { #if !defined(COBALT_BUILD_TYPE_GOLD) d3d12queue_->AddRef(); ULONG reference_count = d3d12queue_->Release(); - SB_DLOG(INFO) << "Reference count of |d3d12queue_| is " - << reference_count; + SB_LOG(INFO) << "Reference count of |d3d12queue_| is " << reference_count; #endif d3d12queue_.Reset(); } @@ -434,11 +487,21 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { #if !defined(COBALT_BUILD_TYPE_GOLD) d3d12device_->AddRef(); ULONG reference_count = d3d12device_->Release(); - SB_DLOG(INFO) << "Reference count of |d3d12device_| is " - << reference_count; + SB_LOG(INFO) << "Reference count of |d3d12device_| is " + << reference_count; #endif d3d12device_.Reset(); } + if (d3d12FrameBuffersHeap_) { +#if !defined(COBALT_BUILD_TYPE_GOLD) + d3d12FrameBuffersHeap_->AddRef(); + ULONG reference_count = d3d12FrameBuffersHeap_->Release(); + SB_LOG(INFO) << "Reference count of |d3d12FrameBuffersHeap_| is " + << reference_count; +#endif + d3d12FrameBuffersHeap_.Reset(); + } + } catch (const std::exception& e) { SB_LOG(ERROR) << "Exception on releasing extended resources: " << e.what(); OnNonrecoverableFailure(); diff --git a/starboard/shared/uwp/extended_resources_manager.h b/starboard/shared/uwp/extended_resources_manager.h index fbd8a5b670bc..54b966f58e78 100644 --- a/starboard/shared/uwp/extended_resources_manager.h +++ b/starboard/shared/uwp/extended_resources_manager.h @@ -47,8 +47,10 @@ class ExtendedResourcesManager { void ReleaseExtendedResources(); void Quit(); - // Returns true when the d3d12 device and command queue can be used. + // Returns true when the d3d12 device, buffer heap + // and command queue can be used. bool GetD3D12Objects(Microsoft::WRL::ComPtr* device, + Microsoft::WRL::ComPtr* buffer_heap, void** command_queue); bool IsGpuDecoderReady() const { @@ -91,6 +93,8 @@ class ExtendedResourcesManager { Queue event_queue_; Microsoft::WRL::ComPtr d3d12device_; Microsoft::WRL::ComPtr d3d12queue_; + // heap for frame buffers (for the decoder and output queue) memory allocation + Microsoft::WRL::ComPtr d3d12FrameBuffersHeap_; // This is set to true when a release of extended resources is requested. // Anything delaying the release should be expedited when this is set. diff --git a/starboard/shared/uwp/player_components_factory.cc b/starboard/shared/uwp/player_components_factory.cc index 69631510b011..ce7f0d26d463 100644 --- a/starboard/shared/uwp/player_components_factory.cc +++ b/starboard/shared/uwp/player_components_factory.cc @@ -236,9 +236,10 @@ class PlayerComponentsFactory : public PlayerComponents::Factory { SB_DCHECK(output_mode == kSbPlayerOutputModeDecodeToTexture); Microsoft::WRL::ComPtr d3d12device; + Microsoft::WRL::ComPtr d3d12buffer_heap; void* d3d12queue = nullptr; if (!uwp::ExtendedResourcesManager::GetInstance()->GetD3D12Objects( - &d3d12device, &d3d12queue)) { + &d3d12device, &d3d12buffer_heap, &d3d12queue)) { // Somehow extended resources get lost. Returns directly to trigger an // error to the player. *error_message = @@ -248,6 +249,7 @@ class PlayerComponentsFactory : public PlayerComponents::Factory { return false; } SB_DCHECK(d3d12device); + SB_DCHECK(d3d12buffer_heap); SB_DCHECK(d3d12queue); #if defined(INTERNAL_BUILD) @@ -258,14 +260,14 @@ class PlayerComponentsFactory : public PlayerComponents::Factory { video_decoder->reset(new GpuVp9VideoDecoder( creation_parameters.decode_target_graphics_context_provider(), creation_parameters.video_stream_info(), is_hdr_video, d3d12device, - d3d12queue)); + d3d12buffer_heap, d3d12queue)); } if (video_codec == kSbMediaVideoCodecAv1) { video_decoder->reset(new GpuAv1VideoDecoder( creation_parameters.decode_target_graphics_context_provider(), creation_parameters.video_stream_info(), is_hdr_video, d3d12device, - d3d12queue)); + d3d12buffer_heap, d3d12queue)); } #endif // defined(INTERNAL_BUILD) diff --git a/starboard/xb1/shared/gpu_base_video_decoder.cc b/starboard/xb1/shared/gpu_base_video_decoder.cc index 32749b16278b..62ff40229a1c 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.cc +++ b/starboard/xb1/shared/gpu_base_video_decoder.cc @@ -16,7 +16,9 @@ #include #include +#include +#include "starboard/once.h" #include "starboard/shared/uwp/application_uwp.h" #include "starboard/shared/uwp/async_utils.h" #include "starboard/shared/uwp/decoder_utils.h" @@ -52,12 +54,77 @@ using Windows::Graphics::Display::Core::HdmiDisplayInformation; // Limit the number of pending buffers. constexpr int kMaxNumberOfPendingBuffers = 8; // Limit the cached presenting images. -constexpr int kNumberOfCachedPresentingImage = 3; +constexpr int kNumberOfCachedPresentingImage = 2; +// The number of frame buffers in decoder +constexpr int kNumOutputFrameBuffers = 7; const char kDecoderThreadName[] = "gpu_video_decoder_thread"; - } // namespace +class GpuFrameBufferPool { + public: + HRESULT AllocateFrameBuffers( + uint16_t width, + uint16_t height, + DXGI_FORMAT dxgi_format, + Microsoft::WRL::ComPtr d3d11_device, + Microsoft::WRL::ComPtr d3d12_device) { + HRESULT hr; + uint16_t number_of_buffers = kNumOutputFrameBuffers; + if (!frame_buffers_.empty()) { + auto& buffer = frame_buffers_.front(); + D3D11_TEXTURE2D_DESC desc; + buffer->texture(0)->GetDesc(&desc); + if (desc.Format != dxgi_format || buffer->width() < width || + buffer->height() < height || + d3d11_device.Get() != buffer->device11().Get() || + d3d12_device.Get() != buffer->device12().Get()) { + frame_buffers_.clear(); + } + } + if (frame_buffers_.empty()) { + frame_buffers_.reserve(number_of_buffers); + while (number_of_buffers--) { + GpuVideoDecoderBase::GpuFrameBuffer* gpu_fb = + new GpuVideoDecoderBase::GpuFrameBuffer(width, height, dxgi_format, + d3d11_device, d3d12_device); + hr = gpu_fb->CreateTextures(); + if (FAILED(hr)) { + frame_buffers_.clear(); + return hr; + } + frame_buffers_.emplace_back(gpu_fb); + } + } + return S_OK; + } + + GpuVideoDecoderBase::GpuFrameBuffer* GetFreeBuffer() { + SB_DCHECK(!frame_buffers_.empty()); + auto iter = std::find_if( + frame_buffers_.begin(), frame_buffers_.end(), + [](const auto& frame_buffer) { return frame_buffer->HasOneRef(); }); + if (iter == frame_buffers_.end()) + return nullptr; + else + return iter->get(); + } + + bool CheckIfAllBuffersAreReleased() { + for (auto&& frame_buffer : frame_buffers_) { + if (!frame_buffer->HasOneRef()) + return false; + } + return true; + } + + private: + std::vector> + frame_buffers_; +}; + +SB_ONCE_INITIALIZE_FUNCTION(GpuFrameBufferPool, GetGpuFrameBufferPool); + class GpuVideoDecoderBase::GPUDecodeTargetPrivate : public SbDecodeTargetPrivate { public: @@ -85,7 +152,6 @@ class GpuVideoDecoderBase::GPUDecodeTargetPrivate info.is_opaque = true; info.width = image->width(); info.height = image->height(); - GLuint gl_textures_yuv[kNumberOfPlanes] = {}; glGenTextures(kNumberOfPlanes, gl_textures_yuv); SB_DCHECK(glGetError() == GL_NO_ERROR); @@ -160,19 +226,109 @@ class GpuVideoDecoderBase::GPUDecodeTargetPrivate void* egl_config_; }; +GpuVideoDecoderBase::GpuFrameBuffer::GpuFrameBuffer( + uint16_t width, + uint16_t height, + DXGI_FORMAT dxgi_format, + Microsoft::WRL::ComPtr d3d11_device, + Microsoft::WRL::ComPtr d3d12_device) + : d3d11_device_(d3d11_device), d3d12_device_(d3d12_device) { + SB_DCHECK(d3d11_device_); + SB_DCHECK(d3d12_device_); + + texture_desc_.Format = dxgi_format; + texture_desc_.Flags = D3D12_RESOURCE_FLAG_ALLOW_RENDER_TARGET; + texture_desc_.DepthOrArraySize = 1; + texture_desc_.MipLevels = 1; + texture_desc_.SampleDesc.Count = 1; + texture_desc_.SampleDesc.Quality = 0; + texture_desc_.Dimension = D3D12_RESOURCE_DIMENSION_TEXTURE2D; + texture_desc_.Layout = D3D12_TEXTURE_LAYOUT_64KB_UNDEFINED_SWIZZLE; + + width_ = width; + height_ = height; +} + +HRESULT GpuVideoDecoderBase::GpuFrameBuffer::CreateTextures() { + const D3D12_HEAP_PROPERTIES kHeapPropertyTypeDefault = { + D3D12_HEAP_TYPE_DEFAULT, D3D12_CPU_PAGE_PROPERTY_UNKNOWN, + D3D12_MEMORY_POOL_UNKNOWN, 1, 1}; + HRESULT hr = E_FAIL; + for (unsigned int i = 0; i < kNumberOfPlanes; i++) { + const int subsampling = i > 0; + const int plane_width = + (texture_desc_.Format == DXGI_FORMAT_R10G10B10A2_UNORM) + ? (((width_ + subsampling) >> subsampling) + 2) / 3 + : ((width_ + subsampling) >> subsampling); + const int plane_height = (height_ + subsampling) >> subsampling; + + // Create interop resources. + texture_desc_.Width = plane_width; + texture_desc_.Height = plane_height; + hr = d3d12_device_->CreateCommittedResource( + &kHeapPropertyTypeDefault, D3D12_HEAP_FLAG_SHARED, &texture_desc_, + D3D12_RESOURCE_STATE_RENDER_TARGET, 0, + IID_PPV_ARGS(&d3d12_resources_[i])); + SB_DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) { + return hr; + } + + // Lowering the priority of texture reduces the amount of texture + // thrashing when the Xbox attempts to transfer textures to faster + // memory as it become more reluctant to be moved. + Microsoft::WRL::ComPtr d3d12_device1; + if (SUCCEEDED(d3d12_device_.As(&d3d12_device1)) && d3d12_device1) { + Microsoft::WRL::ComPtr d3d12_pageable; + if (SUCCEEDED(d3d12_resources_[i].As(&d3d12_pageable)) && + d3d12_pageable) { + D3D12_RESIDENCY_PRIORITY priority = D3D12_RESIDENCY_PRIORITY_LOW; + hr = d3d12_device1->SetResidencyPriority( + 1, d3d12_pageable.GetAddressOf(), &priority); + SB_DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) { + return hr; + } + } + } + + HANDLE interop_handle = 0; + hr = d3d12_device_->CreateSharedHandle(d3d12_resources_[i].Get(), 0, + GENERIC_ALL, NULL, &interop_handle); + SB_DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) { + return hr; + } + hr = d3d11_device_->OpenSharedResource1(interop_handle, + IID_PPV_ARGS(&d3d11_textures_[i])); + SB_DCHECK(SUCCEEDED(hr)); + if (FAILED(hr)) { + return hr; + } + CloseHandle(interop_handle); + } + return S_OK; +} + GpuVideoDecoderBase::GpuVideoDecoderBase( SbDecodeTargetGraphicsContextProvider* decode_target_graphics_context_provider, const VideoStreamInfo& video_stream_info, bool is_hdr_video, + bool is_10x3_preferred, const ComPtr& d3d12_device, + const ComPtr d3d12OutputPoolBufferHeap, void* d3d12_queue) : decode_target_context_runner_(decode_target_graphics_context_provider), is_hdr_video_(is_hdr_video), + is_10x3_preferred_(is_10x3_preferred), d3d12_device_(d3d12_device), - d3d12_queue_(d3d12_queue) { + d3d12_queue_(d3d12_queue), + d3d12FrameBuffersHeap_(d3d12OutputPoolBufferHeap), + frame_buffers_condition_(frame_buffers_mutex_) { SB_DCHECK(d3d12_device_); SB_DCHECK(d3d12_queue_); + SB_DCHECK(d3d12FrameBuffersHeap_); egl_display_ = eglGetDisplay(EGL_DEFAULT_DISPLAY); EGLint attribute_list[] = {EGL_SURFACE_TYPE, // this must be first @@ -215,6 +371,7 @@ GpuVideoDecoderBase::~GpuVideoDecoderBase() { SB_DCHECK(written_inputs_.empty()); SB_DCHECK(output_queue_.empty()); SB_DCHECK(decoder_behavior_.load() == kDecodingStopped); + SB_DCHECK(GetGpuFrameBufferPool()->CheckIfAllBuffersAreReleased()); // All presenting decode targets should be released. SB_DCHECK(presenting_decode_targets_.empty()); @@ -236,9 +393,18 @@ void GpuVideoDecoderBase::Initialize(const DecoderStatusCB& decoder_status_cb, error_cb_ = error_cb; } +size_t GpuVideoDecoderBase::GetPrerollFrameCount() const { + // The underlying decoder has its own output queue. We notify the underlying + // decoder to preroll frames once we receive first needed frame. Then the + // underlying decoder will delay outputs until it has enough prerolled frames. + // When we receive the second output frame, the underlying decoder should + // already have enough prerolled frames in its own output queue. So, we always + // return 2 here. + return 2; +} + size_t GpuVideoDecoderBase::GetMaxNumberOfCachedFrames() const { - return GetMaxNumberOfCachedFramesInternal() - - number_of_presenting_decode_targets_; + return GetMaxNumberOfCachedFramesInternal() + kNumOutputFrameBuffers; } void GpuVideoDecoderBase::WriteInputBuffers(const InputBuffers& input_buffers) { @@ -265,6 +431,7 @@ void GpuVideoDecoderBase::WriteInputBuffers(const InputBuffers& input_buffers) { { ScopedLock pending_inputs_lock(pending_inputs_mutex_); pending_inputs_.push_back(input_buffer); + needs_more_input = pending_inputs_.size() < kMaxNumberOfPendingBuffers; } decoder_behavior_.store(kDecodingFrames); @@ -307,9 +474,13 @@ void GpuVideoDecoderBase::Reset() { decoder_thread_->job_queue()->Schedule( std::bind(&GpuVideoDecoderBase::DrainDecoder, this)); decoder_thread_.reset(); + SB_DCHECK(decoder_behavior_.load() == kDecodingStopped); } pending_inputs_.clear(); - written_inputs_.clear(); + { + ScopedLock input_queue_lock(written_inputs_mutex_); + written_inputs_.clear(); + } // Release all frames after decoder thread is destroyed. decoder_status_cb_(kReleaseAllFrames, nullptr); { @@ -369,30 +540,26 @@ bool GpuVideoDecoderBase::BelongsToDecoderThread() const { return decoder_thread_->job_queue()->BelongsToCurrentThread(); } -void GpuVideoDecoderBase::OnOutputRetrieved( +int GpuVideoDecoderBase::OnOutputRetrieved( const scoped_refptr& image) { SB_DCHECK(decoder_thread_); SB_DCHECK(decoder_status_cb_); SB_DCHECK(image); if (decoder_behavior_.load() == kResettingDecoder || error_occured_) { - return; - } - - if (!BelongsToDecoderThread()) { - decoder_thread_->job_queue()->Schedule( - std::bind(&GpuVideoDecoderBase::OnOutputRetrieved, this, image)); - return; + return 0; } SbTime timestamp = image->timestamp(); - const auto iter = FindByTimestamp(written_inputs_, timestamp); - SB_DCHECK(iter != written_inputs_.cend()); - if (is_hdr_video_) { - image->AttachColorMetadata((*iter)->video_stream_info().color_metadata); + { + ScopedLock input_queue_lock(written_inputs_mutex_); + const auto iter = FindByTimestamp(written_inputs_, timestamp); + SB_DCHECK(iter != written_inputs_.cend()); + if (is_hdr_video_) { + image->AttachColorMetadata((*iter)->video_stream_info().color_metadata); + } + written_inputs_.erase(iter); } - written_inputs_.erase(iter); - scoped_refptr frame(new VideoFrameImpl( timestamp, std::bind(&GpuVideoDecoderBase::DeleteVideoFrame, this, std::placeholders::_1))); @@ -400,10 +567,21 @@ void GpuVideoDecoderBase::OnOutputRetrieved( decoder_behavior_.load() == kEndingStream ? kBufferFull : kNeedMoreInput, frame); + // The underlying decoder relies on the return value of OnOutputRetrieved() to + // determine stream preroll status. The underlying decoder will start + // prorolling at the first time it receives 1 from OnOutputRetrieved(). In + // other words, if OnOutputRetrieved() returns 1, the underlying decoder will + // delay next output until it has enough prerolled frames inside the + // underlying decoder. if (!frame->HasOneRef()) { ScopedLock output_queue_lock(output_queue_mutex_); output_queue_.push_back(image); + if (is_waiting_frame_after_drain_) { + is_waiting_frame_after_drain_ = false; + return 1; + } } + return 0; } void GpuVideoDecoderBase::OnDecoderDrained() { @@ -412,6 +590,7 @@ void GpuVideoDecoderBase::OnDecoderDrained() { SB_DCHECK(decoder_behavior_.load() == kEndingStream || decoder_behavior_.load() == kResettingDecoder); + is_waiting_frame_after_drain_ = true; if (decoder_behavior_.load() == kResettingDecoder || error_occured_) { return; } @@ -445,16 +624,6 @@ void GpuVideoDecoderBase::ReportError(const SbPlayerError error, } } -bool GpuVideoDecoderBase::IsCacheFull() { - SB_DCHECK(decoder_thread_); - SB_DCHECK(BelongsToDecoderThread()); - - ScopedLock output_queue_lock(output_queue_mutex_); - return written_inputs_.size() + output_queue_.size() + - number_of_presenting_decode_targets_ >= - GetMaxNumberOfCachedFramesInternal(); -} - void GpuVideoDecoderBase::DecodeOneBuffer() { SB_DCHECK(decoder_thread_); SB_DCHECK(BelongsToDecoderThread()); @@ -463,20 +632,28 @@ void GpuVideoDecoderBase::DecodeOneBuffer() { return; } - if (IsCacheFull()) { - decoder_thread_->job_queue()->Schedule( - std::bind(&GpuVideoDecoderBase::DecodeOneBuffer, this), - kSbTimeMillisecond); - return; - } - + // Both decoders av1 & vp9 return decoded frames in separate thread, + // so there isn't danger of deadlock in DecodeOneBuffer() and there isn't + // necessity of IsCacheFull call + scoped_refptr input = 0; + bool needs_more_input = false; { ScopedLock pending_inputs_lock(pending_inputs_mutex_); SB_DCHECK(!pending_inputs_.empty()); - written_inputs_.push_back(pending_inputs_.front()); + input = pending_inputs_.front(); pending_inputs_.pop_front(); + if (pending_inputs_.size() < kMaxNumberOfPendingBuffers) { + needs_more_input = true; + } + } + { + ScopedLock input_queue_lock(written_inputs_mutex_); + written_inputs_.push_back(input); + } + if (needs_more_input) { + decoder_status_cb_(kNeedMoreInput, nullptr); } - DecodeInternal(written_inputs_.back()); + DecodeInternal(input); } void GpuVideoDecoderBase::DecodeEndOfStream() { @@ -558,6 +735,58 @@ void GpuVideoDecoderBase::ClearPresentingDecodeTargets() { number_of_presenting_decode_targets_ = 0; } +HRESULT GpuVideoDecoderBase::AllocateFrameBuffers(uint16_t width, + uint16_t height) { + HRESULT hr = S_OK; + DXGI_FORMAT dxgi_format = + is_hdr_video_ ? (is_10x3_preferred_ ? DXGI_FORMAT_R10G10B10A2_UNORM + : DXGI_FORMAT_R16_UNORM) + : DXGI_FORMAT_R8_UNORM; + return GetGpuFrameBufferPool()->AllocateFrameBuffers( + width, height, dxgi_format, d3d11_device_, d3d12_device_); +} + +void GpuVideoDecoderBase::ReleaseFrameBuffer(GpuFrameBuffer* frame_buffer) { + SB_DCHECK(frame_buffer); + ScopedLock lock(frame_buffers_mutex_); + frame_buffer->Release(); + SB_DCHECK(frame_buffer->HasOneRef()); + frame_buffers_condition_.Signal(); +} + +GpuVideoDecoderBase::GpuFrameBuffer* +GpuVideoDecoderBase::GetAvailableFrameBuffer(uint16_t width, uint16_t height) { + if (decoder_behavior_.load() == kResettingDecoder) { + return nullptr; + } + + GpuFrameBuffer* frame_buffer = nullptr; + bool is_resetting = false; + while (!frame_buffer) { + ScopedLock lock(frame_buffers_mutex_); + frame_buffer = GetGpuFrameBufferPool()->GetFreeBuffer(); + // Wait until we get next free frame buffer. + if (!frame_buffer) { + if (is_resetting) { + // We should have enough free frame buffers during resetting. If that + // error happens it means that the frames are not released properly by + // either GpuVideoDecoderBase or VideoRenderer. + SB_NOTREACHED(); + ReportError(kSbPlayerErrorDecode, + "Timed out on waiting for available frame buffer."); + return nullptr; + } + is_resetting = decoder_behavior_.load() == kResettingDecoder; + frame_buffers_condition_.WaitTimed(50 * kSbTimeMillisecond); + continue; + } + } + + // Increment the refcount for |frame_buffer| so that its data buffer + // persists until ReleaseFrameBuffer is called. + frame_buffer->AddRef(); + return frame_buffer; +} } // namespace shared } // namespace xb1 } // namespace starboard diff --git a/starboard/xb1/shared/gpu_base_video_decoder.h b/starboard/xb1/shared/gpu_base_video_decoder.h index e6d2eae8dd1c..de0e45b4480f 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.h +++ b/starboard/xb1/shared/gpu_base_video_decoder.h @@ -51,6 +51,53 @@ class GpuVideoDecoderBase ~GpuVideoDecoderBase() override; + class GpuFrameBuffer : public RefCountedThreadSafe { + public: + GpuFrameBuffer(uint16_t width, + uint16_t height, + DXGI_FORMAT dxgi_format, + Microsoft::WRL::ComPtr d3d11_device, + Microsoft::WRL::ComPtr d3d12_device); + + HRESULT CreateTextures(); + + const Microsoft::WRL::ComPtr& resource(int index) const { + SB_DCHECK(index < kNumberOfPlanes); + SB_DCHECK(d3d12_resources_[index] != nullptr); + return d3d12_resources_[index]; + } + + const Microsoft::WRL::ComPtr& texture(int index) const { + SB_DCHECK(index < kNumberOfPlanes); + SB_DCHECK(d3d11_textures_[index] != nullptr); + return d3d11_textures_[index]; + } + + uint16_t width() const { return width_; } + uint16_t height() const { return height_; } + const Microsoft::WRL::ComPtr& device11() { + return d3d11_device_; + } + const Microsoft::WRL::ComPtr& device12() { + return d3d12_device_; + } + + private: + uint16_t width_ = 0; + uint16_t height_ = 0; + D3D12_RESOURCE_DESC texture_desc_ = {0}; + + Microsoft::WRL::ComPtr d3d12_resources_[kNumberOfPlanes]; + Microsoft::WRL::ComPtr d3d11_textures_[kNumberOfPlanes]; + + const Microsoft::WRL::ComPtr d3d11_device_; + const Microsoft::WRL::ComPtr d3d12_device_; + }; + void ReleaseFrameBuffer(GpuFrameBuffer* frame_buffer); + int GetWidth() { return frame_width_; } + int GetHeight() { return frame_height_; } + bool IsHdrVideo() { return is_hdr_video_; } + protected: typedef ::starboard::shared::starboard::media::VideoStreamInfo VideoStreamInfo; @@ -116,16 +163,20 @@ class GpuVideoDecoderBase kEndingStream }; - GpuVideoDecoderBase(SbDecodeTargetGraphicsContextProvider* - decode_target_graphics_context_provider, - const VideoStreamInfo& video_stream_info, - bool is_hdr_video, - const Microsoft::WRL::ComPtr& d3d12_device, - void* d3d12_queue); + GpuVideoDecoderBase( + SbDecodeTargetGraphicsContextProvider* + decode_target_graphics_context_provider, + const VideoStreamInfo& video_stream_info, + bool is_hdr_video, + bool is_10x3_preferred, + const Microsoft::WRL::ComPtr& d3d12_device, + const Microsoft::WRL::ComPtr d3d12OutputPoolBufferHeap, + void* d3d12_queue); // VideoDecoder methods void Initialize(const DecoderStatusCB& decoder_status_cb, const ErrorCB& error_cb) final; + size_t GetPrerollFrameCount() const final; SbTime GetPrerollTimeout() const final { return kSbTimeMax; } size_t GetMaxNumberOfCachedFrames() const override; @@ -135,19 +186,23 @@ class GpuVideoDecoderBase SbDecodeTarget GetCurrentDecodeTarget() final; // Methods for inherited classes to implement. - virtual size_t GetMaxNumberOfCachedFramesInternal() const = 0; virtual void InitializeCodecIfNeededInternal() = 0; virtual void DecodeInternal( const scoped_refptr& input_buffer) = 0; virtual void DrainDecoderInternal() = 0; + virtual size_t GetMaxNumberOfCachedFramesInternal() const = 0; bool BelongsToDecoderThread() const; - void OnOutputRetrieved(const scoped_refptr& image); void OnDecoderDrained(); void ClearCachedImages(); void ReportError(const SbPlayerError error, const std::string& error_message); + int OnOutputRetrieved(const scoped_refptr& image); + HRESULT AllocateFrameBuffers(uint16_t width, uint16_t height); + GpuVideoDecoderBase::GpuFrameBuffer* GetAvailableFrameBuffer(uint16_t width, + uint16_t height); const bool is_hdr_video_; + const bool is_10x3_preferred_; int frame_width_; int frame_height_; atomic_integral decoder_behavior_{kDecodingStopped}; @@ -156,12 +211,16 @@ class GpuVideoDecoderBase // These are platform-specific objects required to create and use a codec. Microsoft::WRL::ComPtr d3d11_device_; Microsoft::WRL::ComPtr d3d12_device_; + Microsoft::WRL::ComPtr + d3d12FrameBuffersHeap_; // output buffers queue memory void* d3d12_queue_ = nullptr; + Mutex frame_buffers_mutex_; + ConditionVariable frame_buffers_condition_; + // static std::vector> s_frame_buffers_; private: class GPUDecodeTargetPrivate; - bool IsCacheFull(); void DecodeOneBuffer(); void DecodeEndOfStream(); void DrainDecoder(); @@ -183,7 +242,9 @@ class GpuVideoDecoderBase // |pending_inputs_| is shared between player main thread and decoder thread. Mutex pending_inputs_mutex_; std::deque> pending_inputs_; - // |written_inputs_| is only accessed on decoder thread. + // |written_inputs_| is shared between decoder thread and underlying decoder + // output thread. + Mutex written_inputs_mutex_; std::vector> written_inputs_; // |output_queue_| is shared between decoder thread and render thread. Mutex output_queue_mutex_; @@ -195,6 +256,7 @@ class GpuVideoDecoderBase SbMediaColorMetadata last_presented_color_metadata_ = {}; bool is_drain_decoder_called_ = false; + bool is_waiting_frame_after_drain_ = false; bool needs_hdr_metadata_update_ = true; }; From 89532fe0de2a8b53d77005636dae846b0f3d558a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 13:40:31 -0700 Subject: [PATCH 037/140] Cherry pick PR #1415: Update AppxManifest to Windows.Xbox for QA builds (#1416) Refer to the original PR: https://github.com/youtube/cobalt/pull/1415 b/297577690 Co-authored-by: Garo Bournoutian --- starboard/xb1/templates/AppxManifest.xml.template | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starboard/xb1/templates/AppxManifest.xml.template b/starboard/xb1/templates/AppxManifest.xml.template index 85b4a305d3a8..5a2d9f2eab7b 100644 --- a/starboard/xb1/templates/AppxManifest.xml.template +++ b/starboard/xb1/templates/AppxManifest.xml.template @@ -24,10 +24,10 @@ From 367f6bd83db6662779540e2d083f693310a61fe7 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 14:09:12 -0700 Subject: [PATCH 038/140] Cherry pick PR #1446: [Watchdog] Support non-unique client names. (#1595) Refer to the original PR: https://github.com/youtube/cobalt/pull/1446 Support non-unique client names with a parallel data structure client_list_ to client_map_. Adds new register, ping, and unregister functions that return a client pointer to be maintained and used by the caller to identify the client. b/296467581 Change-Id: I48017eda5630ee1ebc6eb5489f22ae6c0934ffe0 Co-authored-by: Brian Ting --- cobalt/watchdog/watchdog.cc | 245 ++++++++++++++++++++++--------- cobalt/watchdog/watchdog.h | 24 ++- cobalt/watchdog/watchdog_test.cc | 122 ++++++++++++--- 3 files changed, 298 insertions(+), 93 deletions(-) diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc index 0c7a45c900e6..0320ec91f4fe 100644 --- a/cobalt/watchdog/watchdog.cc +++ b/cobalt/watchdog/watchdog.cc @@ -207,39 +207,24 @@ void* Watchdog::Monitor(void* context) { starboard::ScopedLock scoped_lock(static_cast(context)->mutex_); while (1) { SbTimeMonotonic current_monotonic_time = SbTimeGetMonotonicNow(); - - // Iterates through client map to monitor all registered clients. bool watchdog_violation = false; + + // Iterates through client map to monitor all name registered clients. for (auto& it : static_cast(context)->client_map_) { Client* client = it.second.get(); - // Ignores and resets clients in idle states, clients whose monitor_state - // is below the current application state. Resets time_wait_microseconds - // and time_interval_microseconds start values. - if (static_cast(context)->state_ > client->monitor_state) { - client->time_registered_monotonic_microseconds = current_monotonic_time; - client->time_last_updated_monotonic_microseconds = - current_monotonic_time; - continue; + if (MonitorClient(context, client, current_monotonic_time)) { + watchdog_violation = true; } + } - SbTimeMonotonic time_delta = - current_monotonic_time - - client->time_last_updated_monotonic_microseconds; - SbTimeMonotonic time_wait = - current_monotonic_time - - client->time_registered_monotonic_microseconds; - - // Watchdog violation - if (time_delta > client->time_interval_microseconds && - time_wait > client->time_wait_microseconds) { + // Iterates through client list to monitor all client registered clients. + for (auto& it : static_cast(context)->client_list_) { + Client* client = it.get(); + if (MonitorClient(context, client, current_monotonic_time)) { watchdog_violation = true; - UpdateViolationsMap(context, client, time_delta); - - // Resets time last updated. - client->time_last_updated_monotonic_microseconds = - current_monotonic_time; } } + if (static_cast(context)->pending_write_) MaybeWriteWatchdogViolations(context); if (watchdog_violation) MaybeTriggerCrash(context); @@ -254,6 +239,33 @@ void* Watchdog::Monitor(void* context) { return nullptr; } +bool Watchdog::MonitorClient(void* context, Client* client, + SbTimeMonotonic current_monotonic_time) { + // Ignores and resets clients in idle states, clients whose monitor_state + // is below the current application state. Resets time_wait_microseconds + // and time_interval_microseconds start values. + if (static_cast(context)->state_ > client->monitor_state) { + client->time_registered_monotonic_microseconds = current_monotonic_time; + client->time_last_updated_monotonic_microseconds = current_monotonic_time; + return false; + } + + SbTimeMonotonic time_delta = + current_monotonic_time - client->time_last_updated_monotonic_microseconds; + SbTimeMonotonic time_wait = + current_monotonic_time - client->time_registered_monotonic_microseconds; + + // Watchdog violation + if (time_delta > client->time_interval_microseconds && + time_wait > client->time_wait_microseconds) { + UpdateViolationsMap(context, client, time_delta); + // Resets time last updated. + client->time_last_updated_monotonic_microseconds = current_monotonic_time; + return true; + } + return false; +} + void Watchdog::UpdateViolationsMap(void* context, Client* client, SbTimeMonotonic time_delta) { // Gets violation dictionary with key client name from violations map. @@ -308,6 +320,9 @@ void Watchdog::UpdateViolationsMap(void* context, Client* client, for (auto& it : static_cast(context)->client_map_) { registered_clients.GetList().emplace_back(base::Value(it.first)); } + for (auto& it : static_cast(context)->client_list_) { + registered_clients.GetList().emplace_back(base::Value(it->name)); + } violation.SetKey("registeredClients", registered_clients.Clone()); // Adds new violation to violations map. @@ -422,19 +437,6 @@ bool Watchdog::Register(std::string name, std::string description, int64_t time_wait_microseconds, Replace replace) { if (is_disabled_) return true; - // Validates parameters. - if (time_interval_microseconds < watchdog_monitor_frequency_ || - time_wait_microseconds < 0) { - SB_DLOG(ERROR) << "[Watchdog] Unable to Register: " << name; - if (time_interval_microseconds < watchdog_monitor_frequency_) { - SB_DLOG(ERROR) << "[Watchdog] Time interval less than min: " - << watchdog_monitor_frequency_; - } else { - SB_DLOG(ERROR) << "[Watchdog] Time wait is negative."; - } - return false; - } - starboard::ScopedLock scoped_lock(mutex_); int64_t current_time = SbTimeToPosix(SbTimeGetNow()); @@ -456,6 +458,65 @@ bool Watchdog::Register(std::string name, std::string description, } } + // Creates new client. + std::unique_ptr client = CreateClient( + name, description, monitor_state, time_interval_microseconds, + time_wait_microseconds, current_time, current_monotonic_time); + if (client == nullptr) return false; + + // Registers. + auto result = client_map_.emplace(name, std::move(client)); + + if (result.second) { + SB_DLOG(INFO) << "[Watchdog] Registered: " << name; + } else { + SB_DLOG(ERROR) << "[Watchdog] Unable to Register: " << name; + } + return result.second; +} + +std::shared_ptr Watchdog::RegisterByClient( + std::string name, std::string description, + base::ApplicationState monitor_state, int64_t time_interval_microseconds, + int64_t time_wait_microseconds) { + if (is_disabled_) return nullptr; + + starboard::ScopedLock scoped_lock(mutex_); + + int64_t current_time = SbTimeToPosix(SbTimeGetNow()); + SbTimeMonotonic current_monotonic_time = SbTimeGetMonotonicNow(); + + // Creates new client. + std::shared_ptr client = CreateClient( + name, description, monitor_state, time_interval_microseconds, + time_wait_microseconds, current_time, current_monotonic_time); + if (client == nullptr) return nullptr; + + // Registers. + client_list_.emplace_back(client); + + SB_DLOG(INFO) << "[Watchdog] Registered: " << name; + return client; +} + +std::unique_ptr Watchdog::CreateClient( + std::string name, std::string description, + base::ApplicationState monitor_state, int64_t time_interval_microseconds, + int64_t time_wait_microseconds, int64_t current_time, + SbTimeMonotonic current_monotonic_time) { + // Validates parameters. + if (time_interval_microseconds < watchdog_monitor_frequency_ || + time_wait_microseconds < 0) { + SB_DLOG(ERROR) << "[Watchdog] Unable to Register: " << name; + if (time_interval_microseconds < watchdog_monitor_frequency_) { + SB_DLOG(ERROR) << "[Watchdog] Time interval less than min: " + << watchdog_monitor_frequency_; + } else { + SB_DLOG(ERROR) << "[Watchdog] Time wait is negative."; + } + return nullptr; + } + // Creates new Client. std::unique_ptr client(new Client); client->name = name; @@ -469,15 +530,7 @@ bool Watchdog::Register(std::string name, std::string description, client->time_last_pinged_microseconds = current_time; client->time_last_updated_monotonic_microseconds = current_monotonic_time; - // Registers. - auto result = client_map_.emplace(name, std::move(client)); - - if (result.second) { - SB_DLOG(INFO) << "[Watchdog] Registered: " << name; - } else { - SB_DLOG(ERROR) << "[Watchdog] Unable to Register: " << name; - } - return result.second; + return std::move(client); } bool Watchdog::Unregister(const std::string& name, bool lock) { @@ -496,11 +549,68 @@ bool Watchdog::Unregister(const std::string& name, bool lock) { return result; } +bool Watchdog::UnregisterByClient(std::shared_ptr client) { + if (is_disabled_) return true; + + starboard::ScopedLock scoped_lock(mutex_); + + std::string name = ""; + if (client) name = client->name; + + // Unregisters. + for (auto it = client_list_.begin(); it != client_list_.end(); it++) { + if (client == *it) { + client_list_.erase(it); + SB_DLOG(INFO) << "[Watchdog] Unregistered: " << name; + return true; + } + } + SB_DLOG(ERROR) << "[Watchdog] Unable to Unregister: " << name; + return false; +} + bool Watchdog::Ping(const std::string& name) { return Ping(name, ""); } bool Watchdog::Ping(const std::string& name, const std::string& info) { if (is_disabled_) return true; + starboard::ScopedLock scoped_lock(mutex_); + + auto it = client_map_.find(name); + bool client_exists = it != client_map_.end(); + + if (client_exists) { + Client* client = it->second.get(); + return PingHelper(client, name, info); + } + SB_DLOG(ERROR) << "[Watchdog] Unable to Ping: " << name; + return false; +} + +bool Watchdog::PingByClient(std::shared_ptr client) { + return PingByClient(client, ""); +} + +bool Watchdog::PingByClient(std::shared_ptr client, + const std::string& info) { + if (is_disabled_) return true; + + std::string name = ""; + if (client) name = client->name; + + starboard::ScopedLock scoped_lock(mutex_); + + for (auto it = client_list_.begin(); it != client_list_.end(); it++) { + if (client == *it) { + return PingHelper(client.get(), name, info); + } + } + SB_DLOG(ERROR) << "[Watchdog] Unable to Ping: " << name; + return false; +} + +bool Watchdog::PingHelper(Client* client, const std::string& name, + const std::string& info) { // Validates parameters. if (info.length() > kWatchdogMaxPingInfoLength) { SB_DLOG(ERROR) << "[Watchdog] Unable to Ping: " << name; @@ -509,36 +619,25 @@ bool Watchdog::Ping(const std::string& name, const std::string& info) { return false; } - starboard::ScopedLock scoped_lock(mutex_); - - auto it = client_map_.find(name); - bool client_exists = it != client_map_.end(); + int64_t current_time = SbTimeToPosix(SbTimeGetNow()); + SbTimeMonotonic current_monotonic_time = SbTimeGetMonotonicNow(); - if (client_exists) { - int64_t current_time = SbTimeToPosix(SbTimeGetNow()); - SbTimeMonotonic current_monotonic_time = SbTimeGetMonotonicNow(); + // Updates last ping. + client->time_last_pinged_microseconds = current_time; + client->time_last_updated_monotonic_microseconds = current_monotonic_time; - Client* client = it->second.get(); - // Updates last ping. - client->time_last_pinged_microseconds = current_time; - client->time_last_updated_monotonic_microseconds = current_monotonic_time; + if (info != "") { + // Creates new ping_info. + base::Value ping_info(base::Value::Type::DICTIONARY); + ping_info.SetKey("timestampMilliseconds", + base::Value(std::to_string(current_time / 1000))); + ping_info.SetKey("info", base::Value(info)); - if (info != "") { - // Creates new ping_info. - base::Value ping_info(base::Value::Type::DICTIONARY); - ping_info.SetKey("timestampMilliseconds", - base::Value(std::to_string(current_time / 1000))); - ping_info.SetKey("info", base::Value(info)); - - client->ping_infos.GetList().emplace_back(ping_info.Clone()); - if (client->ping_infos.GetList().size() > kWatchdogMaxPingInfos) - client->ping_infos.GetList().erase( - client->ping_infos.GetList().begin()); - } - } else { - SB_DLOG(ERROR) << "[Watchdog] Unable to Ping: " << name; + client->ping_infos.GetList().emplace_back(ping_info.Clone()); + if (client->ping_infos.GetList().size() > kWatchdogMaxPingInfos) + client->ping_infos.GetList().erase(client->ping_infos.GetList().begin()); } - return client_exists; + return true; } std::string Watchdog::GetWatchdogViolations( diff --git a/cobalt/watchdog/watchdog.h b/cobalt/watchdog/watchdog.h index 14b0c10744f2..aed2ec1db177 100644 --- a/cobalt/watchdog/watchdog.h +++ b/cobalt/watchdog/watchdog.h @@ -88,9 +88,19 @@ class Watchdog : public Singleton { base::ApplicationState monitor_state, int64_t time_interval_microseconds, int64_t time_wait_microseconds = 0, Replace replace = NONE); + std::shared_ptr RegisterByClient(std::string name, + std::string description, + base::ApplicationState monitor_state, + int64_t time_interval_microseconds, + int64_t time_wait_microseconds = 0); bool Unregister(const std::string& name, bool lock = true); + bool UnregisterByClient(std::shared_ptr client); bool Ping(const std::string& name); bool Ping(const std::string& name, const std::string& info); + bool PingByClient(std::shared_ptr client); + bool PingByClient(std::shared_ptr client, const std::string& info); + bool PingHelper(Client* client, const std::string& name, + const std::string& info); std::string GetWatchdogViolations( const std::vector& clients = {}, bool clear = true); bool GetPersistentSettingWatchdogEnable(); @@ -107,7 +117,16 @@ class Watchdog : public Singleton { std::shared_ptr GetViolationsMap(); void WriteWatchdogViolations(); void EvictOldWatchdogViolations(); + std::unique_ptr CreateClient(std::string name, + std::string description, + base::ApplicationState monitor_state, + int64_t time_interval_microseconds, + int64_t time_wait_microseconds, + int64_t current_time, + SbTimeMonotonic current_monotonic_time); static void* Monitor(void* context); + static bool MonitorClient(void* context, Client* client, + SbTimeMonotonic current_monotonic_time); static void UpdateViolationsMap(void* context, Client* client, SbTimeMonotonic time_delta); static void EvictWatchdogViolation(void* context); @@ -139,8 +158,11 @@ class Watchdog : public Singleton { SbTimeMonotonic time_last_written_microseconds_ = 0; // Number of microseconds between writes. int64_t write_wait_time_microseconds_; - // Dictionary of registered Watchdog clients. + // Dictionary of name registered Watchdog clients. std::unordered_map> client_map_; + // List of client registered Watchdog clients, parallel data structure to + // client_map_. + std::vector> client_list_; // Dictionary of lists of Watchdog violations represented as dictionaries. std::shared_ptr violations_map_; // Monitor thread. diff --git a/cobalt/watchdog/watchdog_test.cc b/cobalt/watchdog/watchdog_test.cc index 7b4d740b96f6..ec3116cf3f95 100644 --- a/cobalt/watchdog/watchdog_test.cc +++ b/cobalt/watchdog/watchdog_test.cc @@ -86,7 +86,6 @@ TEST_F(WatchdogTest, RedundantRegistersShouldFail) { ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, kWatchdogMonitorFrequency)); - ASSERT_FALSE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, kWatchdogMonitorFrequency)); @@ -100,10 +99,6 @@ TEST_F(WatchdogTest, RedundantRegistersShouldFail) { } TEST_F(WatchdogTest, RegisterOnlyAcceptsValidParameters) { - ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", - base::kApplicationStateStarted, - kWatchdogMonitorFrequency, 0)); - ASSERT_TRUE(watchdog_->Unregister("test-name")); ASSERT_FALSE(watchdog_->Register("test-name-1", "test-desc-1", base::kApplicationStateStarted, 1, 0)); ASSERT_FALSE(watchdog_->Unregister("test-name-1")); @@ -127,7 +122,37 @@ TEST_F(WatchdogTest, RegisterOnlyAcceptsValidParameters) { ASSERT_FALSE(watchdog_->Unregister("test-name-6")); } -TEST_F(WatchdogTest, UnmatchedUnregistersShouldFail) { +TEST_F(WatchdogTest, RegisterByClientOnlyAcceptsValidParameters) { + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name", "test-desc", base::kApplicationStateStarted, 1, 0); + ASSERT_EQ(client, nullptr); + ASSERT_FALSE(watchdog_->UnregisterByClient(client)); + client = watchdog_->RegisterByClient("test-name", "test-desc", + base::kApplicationStateStarted, 0, 0); + ASSERT_EQ(client, nullptr); + ASSERT_FALSE(watchdog_->UnregisterByClient(client)); + client = watchdog_->RegisterByClient("test-name", "test-desc", + base::kApplicationStateStarted, -1, 0); + ASSERT_EQ(client, nullptr); + ASSERT_FALSE(watchdog_->UnregisterByClient(client)); + client = watchdog_->RegisterByClient("test-name", "test-desc", + base::kApplicationStateStarted, + kWatchdogMonitorFrequency, 1); + ASSERT_NE(client, nullptr); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); + client = watchdog_->RegisterByClient("test-name", "test-desc", + base::kApplicationStateStarted, + kWatchdogMonitorFrequency, 0); + ASSERT_NE(client, nullptr); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); + client = watchdog_->RegisterByClient("test-name", "test-desc", + base::kApplicationStateStarted, + kWatchdogMonitorFrequency, -1); + ASSERT_EQ(client, nullptr); + ASSERT_FALSE(watchdog_->UnregisterByClient(client)); +} + +TEST_F(WatchdogTest, UnmatchedUnregisterShouldFail) { ASSERT_FALSE(watchdog_->Unregister("test-name")); ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, @@ -136,17 +161,39 @@ TEST_F(WatchdogTest, UnmatchedUnregistersShouldFail) { ASSERT_FALSE(watchdog_->Unregister("test-name")); } -TEST_F(WatchdogTest, UnmatchedPingsShouldFail) { +TEST_F(WatchdogTest, UnmatchedUnregisterByClientShouldFail) { + std::shared_ptr client_test(new Client); + ASSERT_FALSE(watchdog_->UnregisterByClient(client_test)); + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name", "test-desc", base::kApplicationStateStarted, + kWatchdogMonitorFrequency); + ASSERT_NE(client, nullptr); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); + ASSERT_FALSE(watchdog_->UnregisterByClient(client)); +} + +TEST_F(WatchdogTest, UnmatchedPingShouldFail) { ASSERT_FALSE(watchdog_->Ping("test-name")); ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, kWatchdogMonitorFrequency)); ASSERT_TRUE(watchdog_->Ping("test-name")); - ASSERT_TRUE(watchdog_->Ping("test-name")); ASSERT_TRUE(watchdog_->Unregister("test-name")); ASSERT_FALSE(watchdog_->Ping("test-name")); } +TEST_F(WatchdogTest, UnmatchedPingByClientShouldFail) { + std::shared_ptr client_test(new Client); + ASSERT_FALSE(watchdog_->PingByClient(client_test)); + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name", "test-desc", base::kApplicationStateStarted, + kWatchdogMonitorFrequency); + ASSERT_NE(client, nullptr); + ASSERT_TRUE(watchdog_->PingByClient(client)); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); + ASSERT_FALSE(watchdog_->PingByClient(client)); +} + TEST_F(WatchdogTest, PingOnlyAcceptsValidParameters) { ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, @@ -160,6 +207,20 @@ TEST_F(WatchdogTest, PingOnlyAcceptsValidParameters) { ASSERT_TRUE(watchdog_->Unregister("test-name")); } +TEST_F(WatchdogTest, PingByClientOnlyAcceptsValidParameters) { + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name", "test-desc", base::kApplicationStateStarted, + kWatchdogMonitorFrequency); + ASSERT_NE(client, nullptr); + ASSERT_TRUE(watchdog_->PingByClient(client, "42")); + ASSERT_FALSE(watchdog_->PingByClient( + client, + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + "xxxxxxxxxxxxxxxxxxxxxxxxxxx")); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); +} + TEST_F(WatchdogTest, ViolationsJsonShouldPersistAndBeValid) { ASSERT_EQ(watchdog_->GetWatchdogViolations(), ""); ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", @@ -281,11 +342,12 @@ TEST_F(WatchdogTest, ViolationsShouldResetAfterFetch) { std::string json = watchdog_->GetWatchdogViolations(); ASSERT_NE(json.find("test-name-1"), std::string::npos); ASSERT_EQ(json.find("test-name-2"), std::string::npos); - ASSERT_TRUE(watchdog_->Register("test-name-2", "test-desc-2", - base::kApplicationStateStarted, - kWatchdogMonitorFrequency)); + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name-2", "test-desc-2", base::kApplicationStateStarted, + kWatchdogMonitorFrequency); + ASSERT_NE(client, nullptr); SbThreadSleep(kWatchdogSleepDuration); - ASSERT_TRUE(watchdog_->Unregister("test-name-2")); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); json = watchdog_->GetWatchdogViolations(); ASSERT_EQ(json.find("test-name-1"), std::string::npos); ASSERT_NE(json.find("test-name-2"), std::string::npos); @@ -408,7 +470,7 @@ TEST_F(WatchdogTest, TimeWaitShouldPreventViolations) { ASSERT_TRUE(watchdog_->Unregister("test-name")); } -TEST_F(WatchdogTest, PingsShouldPreventViolations) { +TEST_F(WatchdogTest, PingShouldPreventViolations) { ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, kWatchdogMonitorFrequency)); @@ -438,6 +500,20 @@ TEST_F(WatchdogTest, PingsShouldPreventViolations) { ASSERT_TRUE(watchdog_->Unregister("test-name")); } +TEST_F(WatchdogTest, PingByClientShouldPreventViolations) { + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name", "test-desc", base::kApplicationStateStarted, + kWatchdogMonitorFrequency); + SbThreadSleep(kWatchdogMonitorFrequency / 2); + ASSERT_TRUE(watchdog_->PingByClient(client)); + SbThreadSleep(kWatchdogMonitorFrequency / 2); + ASSERT_TRUE(watchdog_->PingByClient(client)); + ASSERT_EQ(watchdog_->GetWatchdogViolations(), ""); + SbThreadSleep(kWatchdogSleepDuration); + ASSERT_NE(watchdog_->GetWatchdogViolations(), ""); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); +} + TEST_F(WatchdogTest, UnregisterShouldPreventViolations) { ASSERT_TRUE(watchdog_->Register("test-name", "test-desc", base::kApplicationStateStarted, @@ -447,6 +523,15 @@ TEST_F(WatchdogTest, UnregisterShouldPreventViolations) { ASSERT_EQ(watchdog_->GetWatchdogViolations(), ""); } +TEST_F(WatchdogTest, UnregisterByClientShouldPreventViolations) { + std::shared_ptr client = watchdog_->RegisterByClient( + "test-name", "test-desc", base::kApplicationStateStarted, + kWatchdogMonitorFrequency); + ASSERT_TRUE(watchdog_->UnregisterByClient(client)); + SbThreadSleep(kWatchdogSleepDuration); + ASSERT_EQ(watchdog_->GetWatchdogViolations(), ""); +} + TEST_F(WatchdogTest, KillSwitchShouldPreventViolations) { TearDown(); watchdog_ = new watchdog::Watchdog(); @@ -502,14 +587,13 @@ TEST_F(WatchdogTest, GetViolationClientNames) { SbThreadSleep(kWatchdogSleepDuration); ASSERT_TRUE(watchdog_->Unregister("test-name-1")); ASSERT_TRUE(watchdog_->Unregister("test-name-2")); + std::vector names = watchdog_->GetWatchdogViolationClientNames(); ASSERT_EQ(names.size(), 2); - std::set expected_names = {"test-name-1", "test-name-2"}; - for (std::vector::const_iterator it = names.begin(); - it != names.end(); ++it) { - const std::string name = *it; - ASSERT_TRUE((expected_names.find(name) != expected_names.end())); - } + ASSERT_TRUE(std::find(names.begin(), names.end(), "test-name-1") != + names.end()); + ASSERT_TRUE(std::find(names.begin(), names.end(), "test-name-2") != + names.end()); watchdog_->GetWatchdogViolations(); names = watchdog_->GetWatchdogViolationClientNames(); ASSERT_EQ(names.size(), 0); From b01d7bf6f213674f9b4e994d72e5ab2f247c76b1 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:08:26 -0700 Subject: [PATCH 039/140] Cherry pick PR #1457: Remove sb_is_modular from base_configuration.gni (#1459) Refer to the original PR: https://github.com/youtube/cobalt/pull/1457 b/295399926 sb_is_modular shouldn't be set by the platform, it's a derived flag already defined in another location. Co-authored-by: Niranjan Yardi --- starboard/build/config/base_configuration.gni | 3 --- 1 file changed, 3 deletions(-) diff --git a/starboard/build/config/base_configuration.gni b/starboard/build/config/base_configuration.gni index 1edd94bc6e6b..6c9d354e5ab1 100644 --- a/starboard/build/config/base_configuration.gni +++ b/starboard/build/config/base_configuration.gni @@ -44,9 +44,6 @@ declare_args() { # Directory path to static contents' data. sb_static_contents_output_data_dir = "$root_out_dir/content/data" - # Whether this is a modular build. - sb_is_modular = false - # Whether this is an Evergreen build. sb_is_evergreen = false From 0f5941f75753f8efbc9fc908999304528421273c Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:08:36 -0700 Subject: [PATCH 040/140] Cherry pick PR #1431: Define wchar_t according to __WCHAR_TYPE__ (#1462) Refer to the original PR: https://github.com/youtube/cobalt/pull/1431 b/298211986 b/246854012 Windows platform modular builds don't work with the current definition of wchar_t for c files. Setting wchar_t according to the predefined C macro __WCHAR_TYPE__ ensures that wchar_t is defined correctly. Error shows up here: b/246854012#comment41 and was discussed in go/lbreview/260401 Co-authored-by: Niranjan Yardi --- third_party/musl/arch/aarch64/bits/alltypes.h | 4 ++++ third_party/musl/arch/arm/bits/alltypes.h | 4 ++++ third_party/musl/arch/x86_64/bits/alltypes.h | 6 ++++-- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/third_party/musl/arch/aarch64/bits/alltypes.h b/third_party/musl/arch/aarch64/bits/alltypes.h index 04d8c0bc33d4..77ff768a550b 100644 --- a/third_party/musl/arch/aarch64/bits/alltypes.h +++ b/third_party/musl/arch/aarch64/bits/alltypes.h @@ -11,10 +11,14 @@ #define __LONG_MAX 0x7fffffffffffffffL #ifndef __cplusplus +#if defined(USE_COBALT_CUSTOMIZATIONS) +typedef __WCHAR_TYPE__ wchar_t; +#else #if defined(__NEED_wchar_t) && !defined(__DEFINED_wchar_t) typedef unsigned wchar_t; #define __DEFINED_wchar_t #endif +#endif // defined(USE_COBALT_CUSTOMIZATIONS) #endif #if defined(__NEED_wint_t) && !defined(__DEFINED_wint_t) diff --git a/third_party/musl/arch/arm/bits/alltypes.h b/third_party/musl/arch/arm/bits/alltypes.h index 2bc88cd728b0..3993b1ea1b20 100644 --- a/third_party/musl/arch/arm/bits/alltypes.h +++ b/third_party/musl/arch/arm/bits/alltypes.h @@ -12,10 +12,14 @@ #define __LONG_MAX 0x7fffffffL #ifndef __cplusplus +#if defined(USE_COBALT_CUSTOMIZATIONS) +typedef __WCHAR_TYPE__ wchar_t; +#else #if defined(__NEED_wchar_t) && !defined(__DEFINED_wchar_t) typedef unsigned wchar_t; #define __DEFINED_wchar_t #endif +#endif // defined(USE_COBALT_CUSTOMIZATIONS) #endif diff --git a/third_party/musl/arch/x86_64/bits/alltypes.h b/third_party/musl/arch/x86_64/bits/alltypes.h index b5d6f523766e..2e3401f5e234 100644 --- a/third_party/musl/arch/x86_64/bits/alltypes.h +++ b/third_party/musl/arch/x86_64/bits/alltypes.h @@ -1,16 +1,18 @@ #define _Addr long #define _Int64 long #define _Reg long - #define __BYTE_ORDER 1234 #define __LONG_MAX 0x7fffffffffffffffL #ifndef __cplusplus +#if defined(USE_COBALT_CUSTOMIZATIONS) +typedef __WCHAR_TYPE__ wchar_t; +#else #if defined(__NEED_wchar_t) && !defined(__DEFINED_wchar_t) typedef int wchar_t; #define __DEFINED_wchar_t #endif - +#endif // defined(USE_COBALT_CUSTOMIZATIONS) #endif #if defined(__FLT_EVAL_METHOD__) && __FLT_EVAL_METHOD__ == 2 From 1bbbde95e9c45a338bd5d70e2afabaf7ceb588a0 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 15:38:47 -0700 Subject: [PATCH 041/140] Cherry pick PR #1596: Update Watchdog crash from CHECK(false). (#1612) Refer to the original PR: https://github.com/youtube/cobalt/pull/1596 Update Watchdog MaybeTriggerCrash() from CHECK(false) to Null Dereference used in H5vccCrashLog::TriggerCrash(). b/300130881 Change-Id: Ie62c371885c50e0320d27415327343421e622f19 Co-authored-by: Brian Ting --- cobalt/watchdog/watchdog.cc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc index 0320ec91f4fe..56e19dd4b1a2 100644 --- a/cobalt/watchdog/watchdog.cc +++ b/cobalt/watchdog/watchdog.cc @@ -427,7 +427,7 @@ void Watchdog::MaybeTriggerCrash(void* context) { if (static_cast(context)->pending_write_) static_cast(context)->WriteWatchdogViolations(); SB_LOG(ERROR) << "[Watchdog] Triggering violation Crash!"; - CHECK(false); + *(reinterpret_cast(0)) = 0; } } From 1264612dba3323b70689eb770e6e1e03bf9878ee Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 16:39:17 -0700 Subject: [PATCH 042/140] Cherry pick PR #1560: [XB1] Use full path for win sdk tools (#1585) Refer to the original PR: https://github.com/youtube/cobalt/pull/1560 Use the full path when accessing windows sdk tools to avoid relying on the PATH variable being set correctly for the running machine. Versions of the WinAppDeployCmd prior to 10.0.22621.0 will fail to install the appx on the newest Xbox firmware. b/299672207 Change-Id: I0c15c52fa9e11281531396c81e00fd0b0eadf4a2 Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/packager.py | 15 +++++++++------ starboard/xb1/tools/xb1_launcher.py | 8 +++++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/starboard/xb1/tools/packager.py b/starboard/xb1/tools/packager.py index 9503f20f24b7..c2ab23dbae56 100644 --- a/starboard/xb1/tools/packager.py +++ b/starboard/xb1/tools/packager.py @@ -52,7 +52,7 @@ 'youtubetv': _INTERNAL_CERT_PATH, } _DEFAULT_SDK_BIN_DIR = 'C:\\Program Files (x86)\\Windows Kits\\10\\bin' -_DEFAULT_WIN_SDK_VERSION = '10.0.22000.0' +_DEFAULT_WIN_SDK_VERSION = '10.0.22621.0' _SOURCE_SPLASH_SCREEN_SUB_PATH = os.path.join('internal', 'cobalt', 'browser', 'splash_screen') # The splash screen file referenced in starboard/xb1/shared/configuration.cc @@ -67,7 +67,7 @@ } -def _SelectBestPath(os_var_name, path): +def _SelectBestPath(os_var_name: str, path: str) -> str: if os_var_name in os.environ: return os.environ[os_var_name] if os.path.exists(path): @@ -86,6 +86,12 @@ def _GetSourceSplashScreenDir(): return os.path.join(src_dir, _SOURCE_SPLASH_SCREEN_SUB_PATH) +def GetWinToolsPath() -> str: + windows_sdk_bin_dir = _SelectBestPath('WindowsSdkBinPath', + _DEFAULT_SDK_BIN_DIR) + return os.path.join(windows_sdk_bin_dir, _DEFAULT_WIN_SDK_VERSION, 'x64') + + class Package(package.PackageBase): """A class representing an installable UWP Appx package.""" @@ -145,10 +151,7 @@ def SupportedPlatforms(cls): return [] def __init__(self, publisher, product, **kwargs): - windows_sdk_bin_dir = _SelectBestPath('WindowsSdkBinPath', - _DEFAULT_SDK_BIN_DIR) - self.windows_sdk_host_tools = os.path.join(windows_sdk_bin_dir, - _DEFAULT_WIN_SDK_VERSION, 'x64') + self.windows_sdk_host_tools = GetWinToolsPath() self.publisher = publisher self.product = product super().__init__(**kwargs) diff --git a/starboard/xb1/tools/xb1_launcher.py b/starboard/xb1/tools/xb1_launcher.py index 969a5ae903a3..ab54abd45660 100644 --- a/starboard/xb1/tools/xb1_launcher.py +++ b/starboard/xb1/tools/xb1_launcher.py @@ -363,10 +363,12 @@ def SignIn(self): self._network_api.SetXboxLiveSignedInUserState(users[0]['EmailAddress'], True) - def WinAppDeployCmd(self, command): + def WinAppDeployCmd(self, command: str): try: - out = subprocess.check_output('WinAppDeployCmd ' + command + ' -ip ' + - self.GetDeviceIp()).decode() + exe_path = os.path.join(packager.GetWinToolsPath(), 'WinAppDeployCmd.exe') + command_str = f'{exe_path} {command} -ip {self.GetDeviceIp()}' + self._LogLn('Running: ' + command_str) + out = subprocess.check_output(command_str).decode() except subprocess.CalledProcessError as e: self._LogLn(e.output) raise e From da26ff7706d55068722d26ab68e1555640762f66 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 20 Sep 2023 18:48:46 -0700 Subject: [PATCH 043/140] Cherry pick PR #1541: Separate modular configs from evergreen (#1549) Refer to the original PR: https://github.com/youtube/cobalt/pull/1541 b/294267479 Refactor flags from evergreen config into a modular config which can be used by any modular platform(windows, linux) Co-authored-by: Niranjan Yardi --- starboard/build/config/modular/BUILD.gn | 213 ++++++++++++++++++ .../shared/platform_configuration/BUILD.gn | 175 +------------- .../platform_configuration/configuration.gni | 6 +- 3 files changed, 218 insertions(+), 176 deletions(-) create mode 100644 starboard/build/config/modular/BUILD.gn diff --git a/starboard/build/config/modular/BUILD.gn b/starboard/build/config/modular/BUILD.gn new file mode 100644 index 000000000000..a243e062d230 --- /dev/null +++ b/starboard/build/config/modular/BUILD.gn @@ -0,0 +1,213 @@ +# Copyright 2023 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. + +config("modular") { + cflags = [ + "-ffunction-sections", + "-fdata-sections", + "-nostdlibinc", + "-isystem" + rebase_path("//third_party/llvm-project/libcxxabi/include", + root_build_dir), + "-isystem" + rebase_path("//third_party/llvm-project/libunwind/include", + root_build_dir), + "-isystem" + rebase_path("//third_party/llvm-project/libcxx/include", + root_build_dir), + "-isystem" + rebase_path("//third_party/musl/include", root_build_dir), + "-isystem" + rebase_path("//third_party/musl/arch/generic", root_build_dir), + ] + + if (!is_host_win) { + # Causes error on windows. clang++: error: unsupported option '-fPIC' for target 'x86_64-pc-windows-msvc' + cflags += [ "-fPIC" ] + } + + cflags_cc = [ + "-nostdinc++", + "-std=c++17", + ] + + defines = [ + # Ensure that the Starboardized __external_threading file is included. + "_LIBCPP_HAS_THREAD_API_EXTERNAL", + + # Ensure that only the forward declarations and type definitions are included + # in __external_threading. + "_LIBCPP_HAS_THREAD_LIBRARY_EXTERNAL", + + # Enable GNU extensions to get prototypes like ffsl. + "_GNU_SOURCE=1", + + "_LIBCPP_HAS_MUSL_LIBC", + "__STDC_FORMAT_MACROS", # so that we get PRI* + + # File format of the shared object we will generate. + "__ELF__", + + # Use scalar portable implementations instead of Clang/GCC vector + # extensions in SkVx.h. + "SKNX_NO_SIMD", + + # By default, pulls in some X11 headers that have some + # nasty macros (|Status|, for example) that conflict with Chromium base. + "MESA_EGL_NO_X11_HEADERS", + ] + + if (is_debug) { + cflags += [ + "-O0", + "-frtti", + ] + if (!(is_host_win && using_old_compiler)) { + cflags += [ + # This flag causes significant increase in shared library binary size on certain windows platforms. Refer b/297357707 + "-g", + ] + } + } else if (is_devel) { + cflags += [ + "-O2", + "-frtti", + ] + if (!(is_host_win && using_old_compiler)) { + cflags += [ + # This flag causes significant increase in shared library binary size on certain windows platforms. Refer b/297357707 + "-g", + ] + } + } else { + cflags += [ "-fno-rtti" ] + if (!(is_host_win && using_old_compiler)) { + cflags += [ + # This flag causes significant increase in shared library binary size on certain windows platforms. Refer b/297357707 + "-gline-tables-only", + ] + } + } + + if (is_clang) { + cflags += [ + "-fcolor-diagnostics", + + # Default visibility to hidden, to enable dead stripping. + "-fvisibility=hidden", + + # Warns on switches on enums that cover all enum values but + # also contain a default: branch. Chrome is full of that. + "-Wno-covered-switch-default", + + # protobuf uses hash_map. + "-Wno-deprecated", + + "-fno-exceptions", + + # Enable unwind tables used by libunwind for stack traces. + "-funwind-tables", + + # Disable usage of frame pointers. + "-fomit-frame-pointer", + + # Don"t warn about the "struct foo f = {0};" initialization pattern. + "-Wno-missing-field-initializers", + + # Do not warn for implicit sign conversions. + "-Wno-sign-conversion", + + "-fno-strict-aliasing", # See http://crbug.com/32204 + + "-Wno-unnamed-type-template-args", + + # Triggered by the COMPILE_ASSERT macro. + "-Wno-unused-local-typedef", + + # Do not warn if a function or variable cannot be implicitly + # instantiated. + "-Wno-undefined-var-template", + + # Do not warn about an implicit exception spec mismatch. + "-Wno-implicit-exception-spec-mismatch", + + # It's OK not to use some input parameters. + "-Wno-unused-parameter", + "-Wno-conversion", + "-Wno-bitwise-op-parentheses", + "-Wno-shift-op-parentheses", + "-Wno-shorten-64-to-32", + "-fno-use-cxa-atexit", + ] + } + + if (is_clang_16) { + cflags += [ + # Do not remove null pointer checks. + "-fno-delete-null-pointer-checks", + ] + } + + if (use_asan) { + cflags += [ + "-fsanitize=address", + "-fno-omit-frame-pointer", + ] + + defines += [ "ADDRESS_SANITIZER" ] + + if (asan_symbolizer_path != "") { + defines += [ "ASAN_SYMBOLIZER_PATH=\"${asan_symbolizer_path}\"" ] + } + } else if (use_tsan) { + cflags += [ + "-fsanitize=thread", + "-fno-omit-frame-pointer", + ] + + defines += [ "THREAD_SANITIZER" ] + } +} + +config("speed") { + cflags = [ "-O2" ] +} + +config("size") { + cflags = [ "-Os" ] +} + +config("pedantic_warnings") { + cflags = [ + "-Wall", + "-Wextra", + "-Wunreachable-code", + ] +} + +config("no_pedantic_warnings") { + cflags = [ + # 'this' pointer cannot be NULL...pointer may be assumed + # to always convert to true. + "-Wno-undefined-bool-conversion", + + # Skia doesn't use overrides. + "-Wno-inconsistent-missing-override", + + # Do not warn for implicit type conversions that may change a value. + "-Wno-conversion", + + # shifting a negative signed value is undefined + "-Wno-shift-negative-value", + + # Width of bit-field exceeds width of its type- value will be truncated + "-Wno-bitfield-width", + "-Wno-undefined-var-template", + ] +} diff --git a/starboard/evergreen/shared/platform_configuration/BUILD.gn b/starboard/evergreen/shared/platform_configuration/BUILD.gn index 186e0f0b7352..3bb69ba43e4d 100644 --- a/starboard/evergreen/shared/platform_configuration/BUILD.gn +++ b/starboard/evergreen/shared/platform_configuration/BUILD.gn @@ -13,6 +13,7 @@ # limitations under the License. config("platform_configuration") { + configs = [ "//starboard/build/config/modular" ] ldflags = [] if (sb_is_evergreen) { ldflags += [ @@ -34,51 +35,8 @@ config("platform_configuration") { if (sb_is_evergreen) { ldflags += [ "-nostdlib" ] } - cflags = [ - "-ffunction-sections", - "-fdata-sections", - "-fPIC", - "-nostdlibinc", - "-isystem" + rebase_path("//third_party/llvm-project/libcxxabi/include", - root_build_dir), - "-isystem" + rebase_path("//third_party/llvm-project/libunwind/include", - root_build_dir), - "-isystem" + rebase_path("//third_party/llvm-project/libcxx/include", - root_build_dir), - "-isystem" + rebase_path("//third_party/musl/include", root_build_dir), - "-isystem" + rebase_path("//third_party/musl/arch/generic", root_build_dir), - ] - - cflags_cc = [ - "-nostdinc++", - "-std=c++17", - ] defines = [ - # Ensure that the Starboardized __external_threading file is included. - "_LIBCPP_HAS_THREAD_API_EXTERNAL", - - # Ensure that only the forward declarations and type definitions are included - # in __external_threading. - "_LIBCPP_HAS_THREAD_LIBRARY_EXTERNAL", - - # Enable GNU extensions to get prototypes like ffsl. - "_GNU_SOURCE=1", - - "_LIBCPP_HAS_MUSL_LIBC", - "__STDC_FORMAT_MACROS", # so that we get PRI* - - # File format of the shared object we will generate. - "__ELF__", - - # Use scalar portable implementations instead of Clang/GCC vector - # extensions in SkVx.h. - "SKNX_NO_SIMD", - - # By default, pulls in some X11 headers that have some - # nasty macros (|Status|, for example) that conflict with Chromium base. - "MESA_EGL_NO_X11_HEADERS", - # During Evergreen updates the CRX package is kept in-memory, instead of # on the file system, before getting unpacked. # TODO(b/158043520): we need to make significant customizations to Chromium @@ -91,150 +49,21 @@ config("platform_configuration") { "IN_MEMORY_UPDATES", ] - if (is_debug) { - cflags += [ - "-O0", - "-frtti", - "-g", - ] - } else if (is_devel) { - cflags += [ - "-O2", - "-frtti", - "-g", - ] - } else { - cflags += [ - "-gline-tables-only", - "-fno-rtti", - ] - } - - if (is_clang) { - cflags += [ - "-fcolor-diagnostics", - - # Default visibility to hidden, to enable dead stripping. - "-fvisibility=hidden", - - # Warns on switches on enums that cover all enum values but - # also contain a default: branch. Chrome is full of that. - "-Wno-covered-switch-default", - - # protobuf uses hash_map. - "-Wno-deprecated", - - "-fno-exceptions", - - # Enable unwind tables used by libunwind for stack traces. - "-funwind-tables", - - # Disable usage of frame pointers. - "-fomit-frame-pointer", - - # Don"t warn about the "struct foo f = {0};" initialization pattern. - "-Wno-missing-field-initializers", - - # Do not warn for implicit sign conversions. - "-Wno-sign-conversion", - - "-fno-strict-aliasing", # See http://crbug.com/32204 - - "-Wno-unnamed-type-template-args", - - # Triggered by the COMPILE_ASSERT macro. - "-Wno-unused-local-typedef", - - # Do not warn if a function or variable cannot be implicitly - # instantiated. - "-Wno-undefined-var-template", - - # Do not warn about an implicit exception spec mismatch. - "-Wno-implicit-exception-spec-mismatch", - - # It's OK not to use some input parameters. - "-Wno-unused-parameter", - "-Wno-conversion", - "-Wno-bitwise-op-parentheses", - "-Wno-shift-op-parentheses", - "-Wno-shorten-64-to-32", - "-fno-use-cxa-atexit", - ] - } - - if (is_clang_16) { - cflags += [ - # Do not remove null pointer checks. - "-fno-delete-null-pointer-checks", - ] - } - if (use_asan) { - cflags += [ - "-fsanitize=address", - "-fno-omit-frame-pointer", - ] - ldflags += [ "-fsanitize=address", # Force linking of the helpers in sanitizer_options.cc "-Wl,-u_sanitizer_options_link_helper", ] - - defines += [ "ADDRESS_SANITIZER" ] - - if (asan_symbolizer_path != "") { - defines += [ "ASAN_SYMBOLIZER_PATH=\"${asan_symbolizer_path}\"" ] - } } else if (use_tsan) { - cflags += [ - "-fsanitize=thread", - "-fno-omit-frame-pointer", - ] - ldflags += [ "-fsanitize=thread" ] - - defines += [ "THREAD_SANITIZER" ] } } -config("speed") { - cflags = [ "-O2" ] -} - config("size") { - cflags = [ "-Os" ] + configs = [ "//starboard/build/config/modular:size" ] if (sb_is_evergreen && (is_qa || is_gold)) { ldflags = [ "-Wl,--icf=safe" ] } } - -config("pedantic_warnings") { - cflags = [ - "-Wall", - "-Wextra", - "-Wunreachable-code", - ] -} - -config("no_pedantic_warnings") { - cflags = [ - # 'this' pointer cannot be NULL...pointer may be assumed - # to always convert to true. - "-Wno-undefined-bool-conversion", - - # Skia doesn't use overrides. - "-Wno-inconsistent-missing-override", - - # Do not warn for implicit type conversions that may change a value. - "-Wno-conversion", - - # shifting a negative signed value is undefined - "-Wno-shift-negative-value", - - # Width of bit-field exceeds width of its type- value will be truncated - "-Wno-bitfield-width", - "-Wno-undefined-var-template", - ] -} diff --git a/starboard/evergreen/shared/platform_configuration/configuration.gni b/starboard/evergreen/shared/platform_configuration/configuration.gni index 4327e1df5cb2..98bf65fce34a 100644 --- a/starboard/evergreen/shared/platform_configuration/configuration.gni +++ b/starboard/evergreen/shared/platform_configuration/configuration.gni @@ -29,13 +29,13 @@ gtest_target_type = "shared_library" starboard_level_final_executable_type = "shared_library" starboard_level_gtest_target_type = "shared_library" -speed_config_path = "//starboard/evergreen/shared/platform_configuration:speed" +speed_config_path = "//starboard/build/config/modular:speed" size_config_path = "//starboard/evergreen/shared/platform_configuration:size" pedantic_warnings_config_path = - "//starboard/evergreen/shared/platform_configuration:pedantic_warnings" + "//starboard/build/config/modular:pedantic_warnings" no_pedantic_warnings_config_path = - "//starboard/evergreen/shared/platform_configuration:no_pedantic_warnings" + "//starboard/build/config/modular:no_pedantic_warnings" cobalt_licenses_platform = "evergreen" From ab19d53ce4c2c861b21826291cf7ee5d615daa33 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 21 Sep 2023 06:45:47 -0700 Subject: [PATCH 044/140] Cherry pick PR #1405: Setup initial enums.xml and actions.xml metadata configurations (#1604) Refer to the original PR: https://github.com/youtube/cobalt/pull/1405 These files are used to support enum Histograms and User Actions respectively. b/297188017 b/296051473 b/296049622 Co-authored-by: Joel Martinez --- tools/metrics/actions/cobalt/actions.xml | 43 +++++++++++++++++++ .../histograms/metadata/cobalt/enums.xml | 42 ++++++++++++++++++ 2 files changed, 85 insertions(+) create mode 100644 tools/metrics/actions/cobalt/actions.xml create mode 100644 tools/metrics/histograms/metadata/cobalt/enums.xml diff --git a/tools/metrics/actions/cobalt/actions.xml b/tools/metrics/actions/cobalt/actions.xml new file mode 100644 index 000000000000..16e1580550aa --- /dev/null +++ b/tools/metrics/actions/cobalt/actions.xml @@ -0,0 +1,43 @@ + + + + + + + diff --git a/tools/metrics/histograms/metadata/cobalt/enums.xml b/tools/metrics/histograms/metadata/cobalt/enums.xml new file mode 100644 index 000000000000..6888e1b7cfbe --- /dev/null +++ b/tools/metrics/histograms/metadata/cobalt/enums.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + A "True/False" enum for boolean histograms. + + + + + + + + From 44f8e3fa3e1143d6b1ee5d15c5cab7a34438c9ae Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 21 Sep 2023 06:47:53 -0700 Subject: [PATCH 045/140] Cherry pick PR #1262: Add histograms.xml for Cobalt (#1609) Refer to the original PR: https://github.com/youtube/cobalt/pull/1262 The first histograms listed are CSS parsing histograms added in https://github.com/youtube/cobalt/pull/1140 b/296049622 Change-Id: I4af436f873e4b1923073aa959116cb67e0835010 Co-authored-by: Joel Martinez --- .../histograms/metadata/cobalt/histograms.xml | 60 +++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tools/metrics/histograms/metadata/cobalt/histograms.xml diff --git a/tools/metrics/histograms/metadata/cobalt/histograms.xml b/tools/metrics/histograms/metadata/cobalt/histograms.xml new file mode 100644 index 000000000000..e958d8c1b5ec --- /dev/null +++ b/tools/metrics/histograms/metadata/cobalt/histograms.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + joeltine@google.com + + A normalized ratio of time to KB of CSS parsing for link tags. Only logs a + sample for links loaded through HTTP/HTTPS and greater than 1KB. See + go/cobalt-js-css-parsing-metrics for the full design. + + + + + + + joeltine@google.com + + A normalized ratio of time to KB of CSS parsing for style tags. Only logs a + sample for HTMLStyleElements with CSS greater than 1KB. See + go/cobalt-js-css-parsing-metrics for the full design. + + + + + + From 07571e6bf355559faccb6069707053058719921f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 21 Sep 2023 11:23:17 -0700 Subject: [PATCH 046/140] Cherry pick PR #1581: Return false for kSbKeyUnknown key events to the Android OS (#1617) Refer to the original PR: https://github.com/youtube/cobalt/pull/1581 b/300368856 Co-authored-by: Colin Liang --- starboard/android/shared/input_events_generator.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/starboard/android/shared/input_events_generator.cc b/starboard/android/shared/input_events_generator.cc index 0b3452421eec..8115b6b1e27f 100644 --- a/starboard/android/shared/input_events_generator.cc +++ b/starboard/android/shared/input_events_generator.cc @@ -679,6 +679,12 @@ bool InputEventsGenerator::CreateInputEventsFromGameActivityEvent( PushKeyEvent(key, type, window_, android_event, events); } + // Cobalt does not handle the kSbKeyUnknown event, return false, + // so the key event can be handled by the next receiver. + if (key == kSbKeyUnknown) { + return false; + } + return true; } From 2dc2d53a863cccfebc2fe31f9b037d208b7941ae Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 10:15:02 -0700 Subject: [PATCH 047/140] Cherry pick PR #1492: Remove obsolete comment about ClientId (#1500) Refer to the original PR: https://github.com/youtube/cobalt/pull/1492 b/286066035 Co-authored-by: Joel Martinez --- cobalt/browser/metrics/cobalt_metrics_service_client.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cobalt/browser/metrics/cobalt_metrics_service_client.cc b/cobalt/browser/metrics/cobalt_metrics_service_client.cc index f52ac1d85803..5856ce6b19e5 100644 --- a/cobalt/browser/metrics/cobalt_metrics_service_client.cc +++ b/cobalt/browser/metrics/cobalt_metrics_service_client.cc @@ -88,7 +88,8 @@ ukm::UkmService* CobaltMetricsServiceClient::GetUkmService() { void CobaltMetricsServiceClient::SetMetricsClientId( const std::string& client_id) { - // TODO(b/286066035): What to do with client id here? + // ClientId is unnecessary within Cobalt. We expect the web client responsible + // for uploading these to have its own concept of device/client identifiers. } // TODO(b/286884542): Audit all stub implementations in this class and reaffirm From 7bd5ada7dc4d07d6bc56446f0f8cf955f1e40fd5 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 13:43:08 -0700 Subject: [PATCH 048/140] Cherry pick PR #1602: [media] Support string parameter in h5vcc::Settings (#1623) Refer to the original PR: https://github.com/youtube/cobalt/pull/1602 1. Allow string input in H5vccSettings::Set. 2. Rename DisableMediaCodec to MediaCodecBlockList. 3. Block only the codecs in the list based on string matching. 4. MediaCodecBlockList blocks only string input separated by semicolon. If it is empty string, it allows all codecs. Examples are: // "av01": block only av1 codec // "avc1;avc3": block only h264 codec // "vp09;vp9": block only vp9 codec // "": allow all codecs b/300950119 Co-authored-by: Bo-Rong Chen --- cobalt/h5vcc/h5vcc_settings.cc | 48 ++++++++++++++------------------- cobalt/h5vcc/h5vcc_settings.h | 5 +++- cobalt/h5vcc/h5vcc_settings.idl | 2 +- 3 files changed, 25 insertions(+), 30 deletions(-) diff --git a/cobalt/h5vcc/h5vcc_settings.cc b/cobalt/h5vcc/h5vcc_settings.cc index e6c7b26e6e86..69aabbf248ac 100644 --- a/cobalt/h5vcc/h5vcc_settings.cc +++ b/cobalt/h5vcc/h5vcc_settings.cc @@ -23,16 +23,6 @@ namespace cobalt { namespace h5vcc { -namespace { -// Only including needed video combinations for the moment. -// option 0 disables all video codecs except h264 -// option 1 disables all video codecs except av1 -// option 2 disables all video codecs except vp9 -constexpr std::array kDisableCodecCombinations{ - {"av01;hev1;hvc1;vp09;vp8.vp9", "avc1;avc3;hev1;hvc1;vp09;vp8;vp9", - "av01;avc1;avc3;hev1;hvc1;vp8"}}; -}; // namespace - H5vccSettings::H5vccSettings( const SetSettingFunc& set_web_setting_func, cobalt::media::MediaModule* media_module, @@ -56,9 +46,9 @@ H5vccSettings::H5vccSettings( persistent_settings_(persistent_settings) { } -bool H5vccSettings::Set(const std::string& name, int32 value) const { +bool H5vccSettings::Set(const std::string& name, SetValueType value) const { const char kMediaPrefix[] = "Media."; - const char kDisableMediaCodec[] = "DisableMediaCodec"; + const char kMediaCodecBlockList[] = "MediaCodecBlockList"; const char kNavigatorUAData[] = "NavigatorUAData"; const char kClientHintHeaders[] = "ClientHintHeaders"; const char kQUIC[] = "QUIC"; @@ -67,35 +57,37 @@ bool H5vccSettings::Set(const std::string& name, int32 value) const { const char kUpdaterMinFreeSpaceBytes[] = "Updater.MinFreeSpaceBytes"; #endif - if (name == kDisableMediaCodec && - value < static_cast(kDisableCodecCombinations.size())) { - can_play_type_handler_->SetDisabledMediaCodecs( - kDisableCodecCombinations[value]); + if (name == kMediaCodecBlockList && value.IsType() && + value.AsType().size() < 256) { + can_play_type_handler_->SetDisabledMediaCodecs(value.AsType()); return true; } - if (set_web_setting_func_ && set_web_setting_func_.Run(name, value)) { + if (set_web_setting_func_ && value.IsType() && + set_web_setting_func_.Run(name, value.AsType())) { return true; } - if (name.rfind(kMediaPrefix, 0) == 0) { - return media_module_ ? media_module_->SetConfiguration( - name.substr(strlen(kMediaPrefix)), value) - : false; + if (name.rfind(kMediaPrefix, 0) == 0 && value.IsType()) { + return media_module_ + ? media_module_->SetConfiguration( + name.substr(strlen(kMediaPrefix)), value.AsType()) + : false; } - if (name.compare(kNavigatorUAData) == 0 && value == 1) { + if (name.compare(kNavigatorUAData) == 0 && value.IsType() && + value.AsType() == 1) { global_environment_->BindTo("userAgentData", user_agent_data_, "navigator"); return true; } - if (name.compare(kClientHintHeaders) == 0) { + if (name.compare(kClientHintHeaders) == 0 && value.IsType()) { if (!persistent_settings_) { return false; } else { persistent_settings_->SetPersistentSetting( network::kClientHintHeadersEnabledPersistentSettingsKey, - std::make_unique(value)); + std::make_unique(value.AsType())); // Tell NetworkModule (if exists) to re-query persistent settings. if (network_module_) { network_module_ @@ -105,13 +97,13 @@ bool H5vccSettings::Set(const std::string& name, int32 value) const { } } - if (name.compare(kQUIC) == 0) { + if (name.compare(kQUIC) == 0 && value.IsType()) { if (!persistent_settings_) { return false; } else { persistent_settings_->SetPersistentSetting( network::kQuicEnabledPersistentSettingsKey, - std::make_unique(value != 0)); + std::make_unique(value.AsType() != 0)); // Tell NetworkModule (if exists) to re-query persistent settings. if (network_module_) { network_module_->SetEnableQuicFromPersistentSettings(); @@ -121,8 +113,8 @@ bool H5vccSettings::Set(const std::string& name, int32 value) const { } #if SB_IS(EVERGREEN) - if (name.compare(kUpdaterMinFreeSpaceBytes) == 0) { - updater_module_->SetMinFreeSpaceBytes(value); + if (name.compare(kUpdaterMinFreeSpaceBytes) == 0 && value.IsType()) { + updater_module_->SetMinFreeSpaceBytes(value.AsType()); return true; } #endif diff --git a/cobalt/h5vcc/h5vcc_settings.h b/cobalt/h5vcc/h5vcc_settings.h index 34e7a440d3a6..f7e10cf56de4 100644 --- a/cobalt/h5vcc/h5vcc_settings.h +++ b/cobalt/h5vcc/h5vcc_settings.h @@ -21,6 +21,7 @@ #include "cobalt/network/network_module.h" #include "cobalt/persistent_storage/persistent_settings.h" #include "cobalt/script/global_environment.h" +#include "cobalt/script/union_type.h" #include "cobalt/script/wrappable.h" #include "cobalt/web/navigator_ua_data.h" @@ -39,6 +40,8 @@ class H5vccSettings : public script::Wrappable { typedef base::Callback SetSettingFunc; + typedef script::UnionType2 SetValueType; + H5vccSettings(const SetSettingFunc& set_web_setting_func, cobalt::media::MediaModule* media_module, cobalt::media::CanPlayTypeHandler* can_play_type_handler, @@ -53,7 +56,7 @@ class H5vccSettings : public script::Wrappable { // Returns true when the setting is set successfully or if the setting has // already been set to the expected value. Returns false when the setting is // invalid or not set to the expected value. - bool Set(const std::string& name, int32 value) const; + bool Set(const std::string& name, SetValueType value) const; DEFINE_WRAPPABLE_TYPE(H5vccSettings); diff --git a/cobalt/h5vcc/h5vcc_settings.idl b/cobalt/h5vcc/h5vcc_settings.idl index fe250c8bbc37..34650a9d1f63 100644 --- a/cobalt/h5vcc/h5vcc_settings.idl +++ b/cobalt/h5vcc/h5vcc_settings.idl @@ -13,5 +13,5 @@ // limitations under the License. interface H5vccSettings { - boolean set(DOMString name, long value); + boolean set(DOMString name, (long or DOMString) value); }; From 811bd12c48298f683308619fb76679867a8ebe73 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:04:23 -0700 Subject: [PATCH 049/140] Cherry pick PR #1468: [media] Pass PlayerWorker::Handler error messages (#1529) Refer to the original PR: https://github.com/youtube/cobalt/pull/1468 PlayerWorker::Handler operations now return a struct HandlerResult, which may contain an error message. This allows detailed error messages to pass from the Handler to the caller. b/291788496 Co-authored-by: Austin Osagie --- .../filter_based_player_worker_handler.cc | 85 ++++++++++--------- .../filter_based_player_worker_handler.h | 24 +++--- .../shared/starboard/player/player_worker.cc | 73 ++++++++++++---- .../shared/starboard/player/player_worker.h | 36 +++++--- 4 files changed, 134 insertions(+), 84 deletions(-) diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc index 56e60fae3813..c2a2beb8457a 100644 --- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc +++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc @@ -39,6 +39,9 @@ namespace { using std::placeholders::_1; using std::placeholders::_2; +typedef shared::starboard::player::PlayerWorker::Handler::HandlerResult + HandlerResult; + // TODO: Make this configurable inside SbPlayerCreate(). const SbTimeMonotonic kUpdateInterval = 200 * kSbTimeMillisecond; @@ -89,7 +92,7 @@ FilterBasedPlayerWorkerHandler::FilterBasedPlayerWorkerHandler( update_job_ = std::bind(&FilterBasedPlayerWorkerHandler::Update, this); } -bool FilterBasedPlayerWorkerHandler::Init( +HandlerResult FilterBasedPlayerWorkerHandler::Init( SbPlayer player, UpdateMediaInfoCB update_media_info_cb, GetPlayerStateCB get_player_state_cb, @@ -126,11 +129,10 @@ bool FilterBasedPlayerWorkerHandler::Init( SB_LOG(ERROR) << "Audio channels requested " << required_audio_channels << ", but currently supported less than or equal to " << supported_audio_channels; - OnError( - kSbPlayerErrorCapabilityChanged, + std::string error_message = FormatString("Required channel %d is greater than maximum channel %d", - required_audio_channels, supported_audio_channels)); - return false; + required_audio_channels, supported_audio_channels); + return {false, std::move(error_message.c_str())}; } } @@ -140,14 +142,14 @@ bool FilterBasedPlayerWorkerHandler::Init( { ::starboard::ScopedLock lock(player_components_existence_mutex_); - std::string error_message; - player_components_ = - factory->CreateComponents(creation_parameters, &error_message); + std::string components_error_message; + player_components_ = factory->CreateComponents(creation_parameters, + &components_error_message); if (!player_components_) { - OnError(kSbPlayerErrorDecode, - FormatString("Failed to create player components with error: %s", - error_message.c_str())); - return false; + std::string error_message = + FormatString("Failed to create player components with error: %s.", + components_error_message.c_str()); + return {false, std::move(error_message.c_str())}; } media_time_provider_ = player_components_->GetMediaTimeProvider(); audio_renderer_ = player_components_->GetAudioRenderer(); @@ -187,16 +189,17 @@ bool FilterBasedPlayerWorkerHandler::Init( update_job_token_ = Schedule(update_job_, kUpdateInterval); - return true; + return {true}; } -bool FilterBasedPlayerWorkerHandler::Seek(SbTime seek_to_time, int ticket) { +HandlerResult FilterBasedPlayerWorkerHandler::Seek(SbTime seek_to_time, + int ticket) { SB_DCHECK(BelongsToCurrentThread()); SB_LOG(INFO) << "Seek to " << seek_to_time << ", and media time provider is " << media_time_provider_; if (!media_time_provider_) { - return false; + return {false, "Invalid media time provider"}; } if (seek_to_time < 0) { @@ -211,10 +214,10 @@ bool FilterBasedPlayerWorkerHandler::Seek(SbTime seek_to_time, int ticket) { media_time_provider_->Seek(seek_to_time); audio_prerolled_ = false; video_prerolled_ = false; - return true; + return {true}; } -bool FilterBasedPlayerWorkerHandler::WriteSamples( +HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( const InputBuffers& input_buffers, int* samples_written) { SB_DCHECK(!input_buffers.empty()); @@ -227,19 +230,19 @@ bool FilterBasedPlayerWorkerHandler::WriteSamples( *samples_written = 0; if (input_buffers.front()->sample_type() == kSbMediaTypeAudio) { if (!audio_renderer_) { - return false; + return {false, "Invalid audio renderer."}; } if (audio_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write audio sample after EOS is reached"; } else { if (!audio_renderer_->CanAcceptMoreData()) { - return true; + return {true}; } for (const auto& input_buffer : input_buffers) { if (input_buffer->drm_info()) { if (!SbDrmSystemIsValid(drm_system_)) { - return false; + return {false, "Invalid DRM system."}; } DumpInputHash(input_buffer); SbDrmSystemPrivate::DecryptStatus decrypt_status = @@ -250,10 +253,10 @@ bool FilterBasedPlayerWorkerHandler::WriteSamples( InputBuffers(input_buffers.begin(), input_buffers.begin() + *samples_written)); } - return true; + return {true}; } if (decrypt_status == SbDrmSystemPrivate::kFailure) { - return false; + return {false, "Sample decryption failure."}; } } DumpInputHash(input_buffer); @@ -265,19 +268,19 @@ bool FilterBasedPlayerWorkerHandler::WriteSamples( SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeVideo); if (!video_renderer_) { - return false; + return {false, "Invalid video renderer."}; } if (video_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write video sample after EOS is reached"; } else { if (!video_renderer_->CanAcceptMoreData()) { - return true; + return {true}; } for (const auto& input_buffer : input_buffers) { if (input_buffer->drm_info()) { if (!SbDrmSystemIsValid(drm_system_)) { - return false; + return {false, "Invalid DRM system."}; } DumpInputHash(input_buffer); SbDrmSystemPrivate::DecryptStatus decrypt_status = @@ -288,10 +291,10 @@ bool FilterBasedPlayerWorkerHandler::WriteSamples( InputBuffers(input_buffers.begin(), input_buffers.begin() + *samples_written)); } - return true; + return {true}; } if (decrypt_status == SbDrmSystemPrivate::kFailure) { - return false; + return {false, "Sample decryption failure."}; } } DumpInputHash(input_buffer); @@ -301,16 +304,17 @@ bool FilterBasedPlayerWorkerHandler::WriteSamples( } } - return true; + return {true}; } -bool FilterBasedPlayerWorkerHandler::WriteEndOfStream(SbMediaType sample_type) { +HandlerResult FilterBasedPlayerWorkerHandler::WriteEndOfStream( + SbMediaType sample_type) { SB_DCHECK(BelongsToCurrentThread()); if (sample_type == kSbMediaTypeAudio) { if (!audio_renderer_) { SB_LOG(INFO) << "Audio EOS enqueued when renderer is NULL."; - return false; + return {false, "Audio EOS enqueued when renderer is NULL."}; } if (audio_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write audio EOS after EOS is enqueued"; @@ -321,7 +325,7 @@ bool FilterBasedPlayerWorkerHandler::WriteEndOfStream(SbMediaType sample_type) { } else { if (!video_renderer_) { SB_LOG(INFO) << "Video EOS enqueued when renderer is NULL."; - return false; + return {false, "Video EOS enqueued when renderer is NULL."}; } if (video_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write video EOS after EOS is enqueued"; @@ -331,17 +335,17 @@ bool FilterBasedPlayerWorkerHandler::WriteEndOfStream(SbMediaType sample_type) { } } - return true; + return {true}; } -bool FilterBasedPlayerWorkerHandler::SetPause(bool pause) { +HandlerResult FilterBasedPlayerWorkerHandler::SetPause(bool pause) { SB_DCHECK(BelongsToCurrentThread()); SB_LOG(INFO) << "Set pause from " << paused_ << " to " << pause << ", and media time provider is " << media_time_provider_; if (!media_time_provider_) { - return false; + return {false, "Invalid media time provider."}; } paused_ = pause; @@ -352,10 +356,11 @@ bool FilterBasedPlayerWorkerHandler::SetPause(bool pause) { media_time_provider_->Play(); } Update(); - return true; + return {true}; } -bool FilterBasedPlayerWorkerHandler::SetPlaybackRate(double playback_rate) { +HandlerResult FilterBasedPlayerWorkerHandler::SetPlaybackRate( + double playback_rate) { SB_DCHECK(BelongsToCurrentThread()); SB_LOG(INFO) << "Set playback rate from " << playback_rate_ << " to " @@ -365,12 +370,12 @@ bool FilterBasedPlayerWorkerHandler::SetPlaybackRate(double playback_rate) { playback_rate_ = playback_rate; if (!media_time_provider_) { - return false; + return {false, "Invalid media time provider."}; } media_time_provider_->SetPlaybackRate(playback_rate_); Update(); - return true; + return {true}; } void FilterBasedPlayerWorkerHandler::SetVolume(double volume) { @@ -385,7 +390,7 @@ void FilterBasedPlayerWorkerHandler::SetVolume(double volume) { } } -bool FilterBasedPlayerWorkerHandler::SetBounds(const Bounds& bounds) { +HandlerResult FilterBasedPlayerWorkerHandler::SetBounds(const Bounds& bounds) { SB_DCHECK(BelongsToCurrentThread()); if (memcmp(&bounds_, &bounds, sizeof(bounds_)) != 0) { @@ -408,7 +413,7 @@ bool FilterBasedPlayerWorkerHandler::SetBounds(const Bounds& bounds) { } } - return true; + return {true}; } void FilterBasedPlayerWorkerHandler::OnError(SbPlayerError error, diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h index 6f3ed97c332a..e55377457367 100644 --- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h +++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.h @@ -48,19 +48,19 @@ class FilterBasedPlayerWorkerHandler : public PlayerWorker::Handler, SbDecodeTargetGraphicsContextProvider* provider); private: - bool Init(SbPlayer player, - UpdateMediaInfoCB update_media_info_cb, - GetPlayerStateCB get_player_state_cb, - UpdatePlayerStateCB update_player_state_cb, - UpdatePlayerErrorCB update_player_error_cb) override; - bool Seek(SbTime seek_to_time, int ticket) override; - bool WriteSamples(const InputBuffers& input_buffers, - int* samples_written) override; - bool WriteEndOfStream(SbMediaType sample_type) override; - bool SetPause(bool pause) override; - bool SetPlaybackRate(double playback_rate) override; + HandlerResult Init(SbPlayer player, + UpdateMediaInfoCB update_media_info_cb, + GetPlayerStateCB get_player_state_cb, + UpdatePlayerStateCB update_player_state_cb, + UpdatePlayerErrorCB update_player_error_cb) override; + HandlerResult Seek(SbTime seek_to_time, int ticket) override; + HandlerResult WriteSamples(const InputBuffers& input_buffers, + int* samples_written) override; + HandlerResult WriteEndOfStream(SbMediaType sample_type) override; + HandlerResult SetPause(bool pause) override; + HandlerResult SetPlaybackRate(double playback_rate) override; void SetVolume(double volume) override; - bool SetBounds(const Bounds& bounds) override; + HandlerResult SetBounds(const Bounds& bounds) override; void Stop() override; void Update(); diff --git a/starboard/shared/starboard/player/player_worker.cc b/starboard/shared/starboard/player/player_worker.cc index 5ee9bc08a3a1..b07f41574d70 100644 --- a/starboard/shared/starboard/player/player_worker.cc +++ b/starboard/shared/starboard/player/player_worker.cc @@ -33,6 +33,9 @@ using std::placeholders::_1; using std::placeholders::_2; using std::placeholders::_3; +typedef shared::starboard::player::PlayerWorker::Handler::HandlerResult + HandlerResult; + #ifdef SB_MEDIA_PLAYER_THREAD_STACK_SIZE const int kPlayerStackSize = SB_MEDIA_PLAYER_THREAD_STACK_SIZE; #else // SB_MEDIA_PLAYER_THREAD_STACK_SIZE @@ -199,15 +202,19 @@ void PlayerWorker::DoInit() { Handler::UpdatePlayerErrorCB update_player_error_cb; update_player_error_cb = std::bind(&PlayerWorker::UpdatePlayerError, this, _1, _2); - if (handler_->Init( - player_, std::bind(&PlayerWorker::UpdateMediaInfo, this, _1, _2, _3), - std::bind(&PlayerWorker::player_state, this), - std::bind(&PlayerWorker::UpdatePlayerState, this, _1), - update_player_error_cb)) { + HandlerResult result = handler_->Init( + player_, std::bind(&PlayerWorker::UpdateMediaInfo, this, _1, _2, _3), + std::bind(&PlayerWorker::player_state, this), + std::bind(&PlayerWorker::UpdatePlayerState, this, _1), + update_player_error_cb); + if (result.success) { UpdatePlayerState(kSbPlayerStateInitialized); } else { - UpdatePlayerError(kSbPlayerErrorDecode, - "Failed to initialize PlayerWorker with unknown error."); + std::string error_message = "Failed to initialize PlayerWorker."; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } + UpdatePlayerError(kSbPlayerErrorDecode, error_message); } } @@ -232,8 +239,13 @@ void PlayerWorker::DoSeek(SbTime seek_to_time, int ticket) { pending_audio_buffers_.clear(); pending_video_buffers_.clear(); - if (!handler_->Seek(seek_to_time, ticket)) { - UpdatePlayerError(kSbPlayerErrorDecode, "Failed seek."); + HandlerResult result = handler_->Seek(seek_to_time, ticket); + if (!result.success) { + std::string error_message = "Failed seek."; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } + UpdatePlayerError(kSbPlayerErrorDecode, error_message); return; } @@ -273,9 +285,14 @@ void PlayerWorker::DoWriteSamples(InputBuffers input_buffers) { SB_DCHECK(pending_video_buffers_.empty()); } int samples_written; - bool result = handler_->WriteSamples(input_buffers, &samples_written); - if (!result) { - UpdatePlayerError(kSbPlayerErrorDecode, "Failed to write sample."); + HandlerResult result = + handler_->WriteSamples(input_buffers, &samples_written); + if (!result.success) { + std::string error_message = "Failed to write sample."; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } + UpdatePlayerError(kSbPlayerErrorDecode, error_message); return; } if (samples_written == input_buffers.size()) { @@ -341,14 +358,24 @@ void PlayerWorker::DoWriteEndOfStream(SbMediaType sample_type) { SB_DCHECK(pending_video_buffers_.empty()); } - if (!handler_->WriteEndOfStream(sample_type)) { - UpdatePlayerError(kSbPlayerErrorDecode, "Failed to write end of stream."); + HandlerResult result = handler_->WriteEndOfStream(sample_type); + if (!result.success) { + std::string error_message = "Failed to write end of stream."; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } + UpdatePlayerError(kSbPlayerErrorDecode, error_message); } } void PlayerWorker::DoSetBounds(Bounds bounds) { SB_DCHECK(job_queue_->BelongsToCurrentThread()); - if (!handler_->SetBounds(bounds)) { + HandlerResult result = handler_->SetBounds(bounds); + if (!result.success) { + std::string error_message = "Failed to set bounds"; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } UpdatePlayerError(kSbPlayerErrorDecode, "Failed to set bounds"); } } @@ -356,15 +383,25 @@ void PlayerWorker::DoSetBounds(Bounds bounds) { void PlayerWorker::DoSetPause(bool pause) { SB_DCHECK(job_queue_->BelongsToCurrentThread()); - if (!handler_->SetPause(pause)) { - UpdatePlayerError(kSbPlayerErrorDecode, "Failed to set pause."); + HandlerResult result = handler_->SetPause(pause); + if (!result.success) { + std::string error_message = "Failed to set pause."; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } + UpdatePlayerError(kSbPlayerErrorDecode, error_message); } } void PlayerWorker::DoSetPlaybackRate(double playback_rate) { SB_DCHECK(job_queue_->BelongsToCurrentThread()); - if (!handler_->SetPlaybackRate(playback_rate)) { + HandlerResult result = handler_->SetPlaybackRate(playback_rate); + if (!result.success) { + std::string error_message = "Failed to set playback rate."; + if (!result.error_message.empty()) { + error_message += " Error: " + result.error_message; + } UpdatePlayerError(kSbPlayerErrorDecode, "Failed to set playback rate."); } } diff --git a/starboard/shared/starboard/player/player_worker.h b/starboard/shared/starboard/player/player_worker.h index cf27fb3ab71e..805ddd382900 100644 --- a/starboard/shared/starboard/player/player_worker.h +++ b/starboard/shared/starboard/player/player_worker.h @@ -76,25 +76,33 @@ class PlayerWorker { const std::string& error_message)> UpdatePlayerErrorCB; + // Stores the success status of Handler operations. If |success| is false, + // |error_message| may be set with details of the error. + typedef struct HandlerResult { + bool success; + std::string error_message; + } HandlerResult; + Handler() = default; virtual ~Handler() {} - // All the following functions return false to signal a fatal error. The - // event processing loop in PlayerWorker will terminate in this case. - virtual bool Init(SbPlayer player, - UpdateMediaInfoCB update_media_info_cb, - GetPlayerStateCB get_player_state_cb, - UpdatePlayerStateCB update_player_state_cb, - UpdatePlayerErrorCB update_player_error_cb) = 0; - virtual bool Seek(SbTime seek_to_time, int ticket) = 0; - virtual bool WriteSamples(const InputBuffers& input_buffers, - int* samples_written) = 0; - virtual bool WriteEndOfStream(SbMediaType sample_type) = 0; - virtual bool SetPause(bool pause) = 0; - virtual bool SetPlaybackRate(double playback_rate) = 0; + // All the following functions set |HandlerResult.success| to false to + // signal a fatal error. The event processing loop in PlayerWorker will + // terminate in this case. + virtual HandlerResult Init(SbPlayer player, + UpdateMediaInfoCB update_media_info_cb, + GetPlayerStateCB get_player_state_cb, + UpdatePlayerStateCB update_player_state_cb, + UpdatePlayerErrorCB update_player_error_cb) = 0; + virtual HandlerResult Seek(SbTime seek_to_time, int ticket) = 0; + virtual HandlerResult WriteSamples(const InputBuffers& input_buffers, + int* samples_written) = 0; + virtual HandlerResult WriteEndOfStream(SbMediaType sample_type) = 0; + virtual HandlerResult SetPause(bool pause) = 0; + virtual HandlerResult SetPlaybackRate(double playback_rate) = 0; virtual void SetVolume(double volume) = 0; - virtual bool SetBounds(const Bounds& bounds) = 0; + virtual HandlerResult SetBounds(const Bounds& bounds) = 0; // Once this function returns, all processing on the Handler and related // objects has to be stopped. The JobQueue will be destroyed immediately From 2db21fde3e437afe01bcc9a863f8987eaeadbd31 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:32:20 -0700 Subject: [PATCH 050/140] Cherry pick PR #1550: Refactor evergreen x64 compiler flags to modular x64 (#1552) Refer to the original PR: https://github.com/youtube/cobalt/pull/1550 b/294267479 Refactor flags from evergreen x64 config into a modular x64 config which can be used for any modular platform(windows, linux) Change-Id: Ie198d63bfc44207488def4352636d5f85f4e9ba5 Co-authored-by: Niranjan Yardi --- starboard/build/config/modular/x64/BUILD.gn | 31 +++++++++++++++++++ .../x64/platform_configuration/BUILD.gn | 13 +------- 2 files changed, 32 insertions(+), 12 deletions(-) create mode 100644 starboard/build/config/modular/x64/BUILD.gn diff --git a/starboard/build/config/modular/x64/BUILD.gn b/starboard/build/config/modular/x64/BUILD.gn new file mode 100644 index 000000000000..3eb0f5354353 --- /dev/null +++ b/starboard/build/config/modular/x64/BUILD.gn @@ -0,0 +1,31 @@ +# Copyright 2023 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. + +config("sabi_flags") { + cflags = [ + "-march=x86-64", + "-target", + "x86_64-unknown-linux-elf", + ] +} + +config("x64") { + configs = [ + "//starboard/build/config/sabi", + ":sabi_flags", + ] + + cflags = [ "-isystem" + + rebase_path("//third_party/musl/arch/x86_64", root_build_dir) ] +} diff --git a/starboard/evergreen/x64/platform_configuration/BUILD.gn b/starboard/evergreen/x64/platform_configuration/BUILD.gn index d49869b455e9..ead85eed7735 100644 --- a/starboard/evergreen/x64/platform_configuration/BUILD.gn +++ b/starboard/evergreen/x64/platform_configuration/BUILD.gn @@ -12,18 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. -config("sabi_flags") { - cflags = [ - "-march=x86-64", - "-target", - "x86_64-unknown-linux-elf", - ] -} - config("platform_configuration") { configs = [ - "//starboard/build/config/sabi", - ":sabi_flags", + "//starboard/build/config/modular/x64", "//starboard/evergreen/shared/platform_configuration", ] @@ -31,6 +22,4 @@ config("platform_configuration") { "-Wl,-m", "-Wl,elf_x86_64", ] - cflags = [ "-isystem" + - rebase_path("//third_party/musl/arch/x86_64", root_build_dir) ] } From 188d1a02300fcce63df3fea9b671312bcf84f488 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 17:36:17 -0700 Subject: [PATCH 051/140] Cherry pick PR #1375: Disable audio_dmp_player for windows modular builds (#1610) Refer to the original PR: https://github.com/youtube/cobalt/pull/1375 b/297202004 This test target causes link errors on windows modular builds Interestingly i see the same issue locally with monolithic builds as well - im not sure how it builds in CI Co-authored-by: Niranjan Yardi --- starboard/BUILD.gn | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/starboard/BUILD.gn b/starboard/BUILD.gn index 3c9fc83c9b2b..aab526b978eb 100644 --- a/starboard/BUILD.gn +++ b/starboard/BUILD.gn @@ -52,8 +52,12 @@ group("gn_all") { deps += [ "//starboard/shared/starboard/player/filter/testing:player_filter_tests($starboard_toolchain)", "//starboard/shared/starboard/player/filter/testing:player_filter_tests_install($starboard_toolchain)", - "//starboard/shared/starboard/player/filter/tools:audio_dmp_player($starboard_toolchain)", ] + + # TODO: b/296715826 - Fix build error for windows modular builds. + if (!(sb_is_modular && is_host_win)) { + deps += [ "//starboard/shared/starboard/player/filter/tools:audio_dmp_player($starboard_toolchain)" ] + } } if (sb_enable_benchmark) { From 97cdd1bcea2d2090e67730d05f37dda615fc62ab Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:22:02 -0700 Subject: [PATCH 052/140] Cherry pick PR #1418: Remove multiple_player_test.cc from nplb targets for windows modular builds (#1426) Refer to the original PR: https://github.com/youtube/cobalt/pull/1418 b/297808555 mutiple_player_test.cc depends on non standard starboard symbols and will be temporarily disabled for windows modular builds. See error for multiple_player_test here b/297808555#comment2 Change-Id: I15bc5ffddbbe19e1e8e577de93f0cd6f8ef645b3 Co-authored-by: Niranjan Yardi --- starboard/nplb/BUILD.gn | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/starboard/nplb/BUILD.gn b/starboard/nplb/BUILD.gn index 664751bdc1ac..3f314cf427d5 100644 --- a/starboard/nplb/BUILD.gn +++ b/starboard/nplb/BUILD.gn @@ -271,7 +271,7 @@ target(gtest_target_type, "nplb") { cflags = [ "-Wno-enum-constexpr-conversion" ] } - # TODO b/296238576 Add these tests for windows based platform modular builds + # TODO: b/297808555 - Add these tests for windows based platform modular builds. if (sb_is_modular && !sb_is_evergreen && is_host_win) { sources -= [ "maximum_player_configuration_explorer.cc", @@ -279,6 +279,7 @@ target(gtest_target_type, "nplb") { "maximum_player_configuration_explorer_test.cc", "media_buffer_test.cc", "media_set_audio_write_duration_test.cc", + "multiple_player_test.cc", "player_create_test.cc", "player_creation_param_helpers.cc", "player_creation_param_helpers.h", From 9cb22b762251a6dad6ab0051f5e8a8ea6afea59d Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:22:50 -0700 Subject: [PATCH 053/140] Cherry pick PR #1489: Don't use starboard_platform_group install target for bundled platforms (#1629) Refer to the original PR: https://github.com/youtube/cobalt/pull/1489 b/298729481 Add modular loader dependency on starboard install target for platforms which don't have bundle test targets Co-authored-by: Niranjan Yardi --- starboard/build/config/BUILDCONFIG.gn | 4 +++- starboard/raspi/2/platform_configuration/configuration.gni | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn index b60ded19243a..a5feda72530a 100644 --- a/starboard/build/config/BUILDCONFIG.gn +++ b/starboard/build/config/BUILDCONFIG.gn @@ -440,8 +440,10 @@ template("shared_library") { deps = [ ":$original_target_name($cobalt_toolchain)", "//starboard:starboard_platform_group($starboard_toolchain)", - "//starboard:starboard_platform_group_install($starboard_toolchain)", ] + if (!separate_install_targets_for_bundling) { + deps += [ "//starboard:starboard_platform_group_install($starboard_toolchain)" ] + } } copy("${actual_target_name}_loader_copy") { forward_variables_from(invoker, [ "testonly" ]) diff --git a/starboard/raspi/2/platform_configuration/configuration.gni b/starboard/raspi/2/platform_configuration/configuration.gni index 8a175e1779e1..ad65232ac153 100644 --- a/starboard/raspi/2/platform_configuration/configuration.gni +++ b/starboard/raspi/2/platform_configuration/configuration.gni @@ -19,6 +19,8 @@ if (current_toolchain != default_toolchain || sb_evergreen_compatible_use_libunwind = true sb_is_evergreen_compatible = true +} +if (!build_with_separate_cobalt_toolchain) { separate_install_targets_for_bundling = true } From 705ef06688392515c1db531d6c89f450e87a5e42 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 22 Sep 2023 18:25:49 -0700 Subject: [PATCH 054/140] Cherry pick PR #1551: Include "-fno-delete-null-pointer-checks" for windows modular builds (#1631) Refer to the original PR: https://github.com/youtube/cobalt/pull/1551 b/246854012 Include "-fno-delete-null-pointer-checks" for windows modular builds Store variables in a .gni file which will be imported for all modular builds. See error which triggered this change: b/246854012#comment88 Co-authored-by: Niranjan Yardi --- starboard/build/config/modular/BUILD.gn | 2 +- starboard/build/config/modular/variables.gni | 20 ++++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 starboard/build/config/modular/variables.gni diff --git a/starboard/build/config/modular/BUILD.gn b/starboard/build/config/modular/BUILD.gn index a243e062d230..231b6a187e81 100644 --- a/starboard/build/config/modular/BUILD.gn +++ b/starboard/build/config/modular/BUILD.gn @@ -147,7 +147,7 @@ config("modular") { ] } - if (is_clang_16) { + if (is_clang_16 || is_host_win) { cflags += [ # Do not remove null pointer checks. "-fno-delete-null-pointer-checks", diff --git a/starboard/build/config/modular/variables.gni b/starboard/build/config/modular/variables.gni new file mode 100644 index 000000000000..7b220fa73690 --- /dev/null +++ b/starboard/build/config/modular/variables.gni @@ -0,0 +1,20 @@ +# Copyright 2023 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. + +assert(current_toolchain == default_toolchain, + "Cannot access variables for non-default toolchains") + +if (!is_host_win) { + is_clang_16 = true +} From c75f3f3ce8a175cc870c70e4eaa883380712181d Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:32:01 -0700 Subject: [PATCH 055/140] Cherry pick PR #1516: Create intermediate cobalt toolchain for native linker modular builds (#1523) Refer to the original PR: https://github.com/youtube/cobalt/pull/1516 b/294450490 Make an intermediate cobalt toolchain template which can be used for native linker modular builds. Co-authored-by: Niranjan Yardi --- starboard/build/config/BUILDCONFIG.gn | 2 +- .../build/toolchain/cobalt_toolchains.gni | 52 +++++++++++++++++++ starboard/linux/x64x11/toolchain/BUILD.gn | 7 +++ starboard/raspi/2/toolchain/BUILD.gn | 7 +++ starboard/raspi/2/toolchain/variables.gni | 17 ------ 5 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 starboard/build/toolchain/cobalt_toolchains.gni delete mode 100644 starboard/raspi/2/toolchain/variables.gni diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn index a5feda72530a..91a650583ba8 100644 --- a/starboard/build/config/BUILDCONFIG.gn +++ b/starboard/build/config/BUILDCONFIG.gn @@ -99,7 +99,7 @@ if (target_cpu == "x86" || target_cpu == "arm") { host_toolchain = "//starboard/build/toolchain/$host_os:$_host_toolchain_cpu" if (build_with_separate_cobalt_toolchain) { - cobalt_toolchain = "//starboard/build/toolchain:clang" + cobalt_toolchain = "//$starboard_path/toolchain:cobalt" starboard_toolchain = "//$starboard_path/toolchain:starboard" } else { cobalt_toolchain = "//$starboard_path/toolchain:target" diff --git a/starboard/build/toolchain/cobalt_toolchains.gni b/starboard/build/toolchain/cobalt_toolchains.gni new file mode 100644 index 000000000000..f88cab4c2d5f --- /dev/null +++ b/starboard/build/toolchain/cobalt_toolchains.gni @@ -0,0 +1,52 @@ +# Copyright 2023 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. + +import("//build/config/win/visual_studio_version.gni") +import("//build/toolchain/gcc_toolchain.gni") + +template("cobalt_clang_toolchain") { + gcc_toolchain(target_name) { + forward_variables_from(invoker.variables, + [ + "native_linker_path", + "executable_extension", + "shlib_extension", + ]) + assert(defined(native_linker_path), + "native_linker_path has to be defined by the platform") + if (!is_host_win) { + prefix = rebase_path("$clang_base_path/bin", root_build_dir) + cc = "$prefix/clang" + cxx = "$prefix/clang++" + ld = native_linker_path + readelf = "readelf" + ar = "${prefix}/llvm-ar" + nm = "nm" + } else { + prefix = llvm_clang_path + cc = "$prefix/clang.exe" + cxx = "$prefix/clang++.exe" + ld = native_linker_path + readelf = "$prefix/llvm-readobj.exe" + ar = "${prefix}/llvm-ar.exe" + nm = "${prefix}/llvm-nm.exe" + } + toolchain_args = { + if (defined(invoker.toolchain_args)) { + forward_variables_from(invoker.toolchain_args, "*") + } + is_clang = true + } + } +} diff --git a/starboard/linux/x64x11/toolchain/BUILD.gn b/starboard/linux/x64x11/toolchain/BUILD.gn index bcee641a542b..79b79916ef8b 100644 --- a/starboard/linux/x64x11/toolchain/BUILD.gn +++ b/starboard/linux/x64x11/toolchain/BUILD.gn @@ -13,12 +13,19 @@ # limitations under the License. import("//build/config/clang/clang.gni") +import("//starboard/build/toolchain/cobalt_toolchains.gni") import("//starboard/shared/toolchain/overridable_gcc_toolchain.gni") overridable_clang_toolchain("starboard") { clang_base_path = clang_base_path } +cobalt_clang_toolchain("cobalt") { + variables = { + native_linker_path = "$clang_base_path/bin/clang++" + } +} + overridable_clang_toolchain("target") { clang_base_path = clang_base_path } diff --git a/starboard/raspi/2/toolchain/BUILD.gn b/starboard/raspi/2/toolchain/BUILD.gn index 676849572bda..2c49b20d29a7 100644 --- a/starboard/raspi/2/toolchain/BUILD.gn +++ b/starboard/raspi/2/toolchain/BUILD.gn @@ -13,8 +13,15 @@ # limitations under the License. import("//build/toolchain/gcc_toolchain.gni") +import("//starboard/build/toolchain/cobalt_toolchains.gni") import("//starboard/raspi/shared/toolchain/raspi_shared_toolchain.gni") +cobalt_clang_toolchain("cobalt") { + variables = { + native_linker_path = gcc_toolchain_cxx + } +} + gcc_toolchain("starboard") { cc = gcc_toolchain_cc cxx = gcc_toolchain_cxx diff --git a/starboard/raspi/2/toolchain/variables.gni b/starboard/raspi/2/toolchain/variables.gni deleted file mode 100644 index 7493d34befa4..000000000000 --- a/starboard/raspi/2/toolchain/variables.gni +++ /dev/null @@ -1,17 +0,0 @@ -# Copyright 2023 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. - -import("//starboard/raspi/shared/toolchain/raspi_shared_toolchain.gni") - -native_linker_path = gcc_toolchain_cxx From 05c8051d10aecd605a14c9038bd272571b3538f1 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:32:38 -0700 Subject: [PATCH 056/140] Cherry pick PR #1621: Remove debug info from non-production CI builds (#1630) Refer to the original PR: https://github.com/youtube/cobalt/pull/1621 b/301163476 Remove "-g" for devel, debug CI builds - this helps reduce binary sizes on CI while not affecting debug info locally. Add "-gline-tables-only" for older windows platforms to get debuggable info locally and in production. Co-authored-by: Niranjan Yardi --- starboard/build/config/modular/BUILD.gn | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/starboard/build/config/modular/BUILD.gn b/starboard/build/config/modular/BUILD.gn index 231b6a187e81..d266109b94de 100644 --- a/starboard/build/config/modular/BUILD.gn +++ b/starboard/build/config/modular/BUILD.gn @@ -68,9 +68,9 @@ config("modular") { "-O0", "-frtti", ] - if (!(is_host_win && using_old_compiler)) { + if (!cobalt_fastbuild) { cflags += [ - # This flag causes significant increase in shared library binary size on certain windows platforms. Refer b/297357707 + # This flag causes an increase in binary size on certain platforms. Refer b/297357707 "-g", ] } @@ -79,20 +79,17 @@ config("modular") { "-O2", "-frtti", ] - if (!(is_host_win && using_old_compiler)) { + if (!cobalt_fastbuild) { cflags += [ - # This flag causes significant increase in shared library binary size on certain windows platforms. Refer b/297357707 + # This flag causes an increase in binary size on certain platforms. Refer b/297357707 "-g", ] } } else { - cflags += [ "-fno-rtti" ] - if (!(is_host_win && using_old_compiler)) { - cflags += [ - # This flag causes significant increase in shared library binary size on certain windows platforms. Refer b/297357707 - "-gline-tables-only", - ] - } + cflags += [ + "-fno-rtti", + "-gline-tables-only", + ] } if (is_clang) { From 72f63a39a1e245536b54229c402b356972c78656 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Sat, 23 Sep 2023 19:35:37 -0700 Subject: [PATCH 057/140] Cherry pick PR #1512: Remove loader executable copy action for windows based modular platforms (#1517) Refer to the original PR: https://github.com/youtube/cobalt/pull/1512 b/246854012 The copy action has to be removed for windows based platforms as the executable extension is unknown in BUILDCONFIG Co-authored-by: Niranjan Yardi --- starboard/build/config/BUILDCONFIG.gn | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn index 91a650583ba8..022b8a156fef 100644 --- a/starboard/build/config/BUILDCONFIG.gn +++ b/starboard/build/config/BUILDCONFIG.gn @@ -409,9 +409,11 @@ template("shared_library") { forward_variables_from(invoker, [ "testonly" ]) deps = [ ":${actual_target_name}_loader($starboard_toolchain)", - ":${actual_target_name}_loader_copy($starboard_toolchain)", ":${actual_target_name}_loader_install($starboard_toolchain)", ] + if (!is_host_win) { + deps += [ ":${actual_target_name}_loader_copy($starboard_toolchain)" ] + } } if (current_toolchain == starboard_toolchain) { executable("${actual_target_name}_loader") { @@ -445,11 +447,13 @@ template("shared_library") { deps += [ "//starboard:starboard_platform_group_install($starboard_toolchain)" ] } } - copy("${actual_target_name}_loader_copy") { - forward_variables_from(invoker, [ "testonly" ]) - sources = [ "$root_out_dir/${actual_target_name}_loader" ] - outputs = [ "$root_build_dir/${actual_target_name}_loader" ] - deps = [ ":${actual_target_name}_loader" ] + if (!is_host_win) { + copy("${actual_target_name}_loader_copy") { + forward_variables_from(invoker, [ "testonly" ]) + sources = [ "$root_out_dir/${actual_target_name}_loader" ] + outputs = [ "$root_build_dir/${actual_target_name}_loader" ] + deps = [ ":${actual_target_name}_loader" ] + } } } } From 320aafb13b11386e2581a267730aadec4598c107 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 25 Sep 2023 10:08:13 -0700 Subject: [PATCH 058/140] Cherry pick PR #1559: [media] Refine HandlerResult use (#1628) Refer to the original PR: https://github.com/youtube/cobalt/pull/1559 Reduces code duplication and errors surround the use of PlayerWorker::Handler::HandlerResult. b/291788496 Co-authored-by: Austin Osagie --- .../filter_based_player_worker_handler.cc | 48 +++++++-------- .../shared/starboard/player/player_worker.cc | 60 +++++++------------ .../shared/starboard/player/player_worker.h | 18 +++--- 3 files changed, 55 insertions(+), 71 deletions(-) diff --git a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc index c2a2beb8457a..0816518b90e1 100644 --- a/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc +++ b/starboard/shared/starboard/player/filter/filter_based_player_worker_handler.cc @@ -132,7 +132,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::Init( std::string error_message = FormatString("Required channel %d is greater than maximum channel %d", required_audio_channels, supported_audio_channels); - return {false, std::move(error_message.c_str())}; + return HandlerResult{false, error_message}; } } @@ -149,7 +149,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::Init( std::string error_message = FormatString("Failed to create player components with error: %s.", components_error_message.c_str()); - return {false, std::move(error_message.c_str())}; + return HandlerResult{false, error_message}; } media_time_provider_ = player_components_->GetMediaTimeProvider(); audio_renderer_ = player_components_->GetAudioRenderer(); @@ -189,7 +189,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::Init( update_job_token_ = Schedule(update_job_, kUpdateInterval); - return {true}; + return HandlerResult{true}; } HandlerResult FilterBasedPlayerWorkerHandler::Seek(SbTime seek_to_time, @@ -199,7 +199,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::Seek(SbTime seek_to_time, SB_LOG(INFO) << "Seek to " << seek_to_time << ", and media time provider is " << media_time_provider_; if (!media_time_provider_) { - return {false, "Invalid media time provider"}; + return HandlerResult{false, "Invalid media time provider"}; } if (seek_to_time < 0) { @@ -214,7 +214,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::Seek(SbTime seek_to_time, media_time_provider_->Seek(seek_to_time); audio_prerolled_ = false; video_prerolled_ = false; - return {true}; + return HandlerResult{true}; } HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( @@ -230,19 +230,19 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( *samples_written = 0; if (input_buffers.front()->sample_type() == kSbMediaTypeAudio) { if (!audio_renderer_) { - return {false, "Invalid audio renderer."}; + return HandlerResult{false, "Invalid audio renderer."}; } if (audio_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write audio sample after EOS is reached"; } else { if (!audio_renderer_->CanAcceptMoreData()) { - return {true}; + return HandlerResult{true}; } for (const auto& input_buffer : input_buffers) { if (input_buffer->drm_info()) { if (!SbDrmSystemIsValid(drm_system_)) { - return {false, "Invalid DRM system."}; + return HandlerResult{false, "Invalid DRM system."}; } DumpInputHash(input_buffer); SbDrmSystemPrivate::DecryptStatus decrypt_status = @@ -253,10 +253,10 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( InputBuffers(input_buffers.begin(), input_buffers.begin() + *samples_written)); } - return {true}; + return HandlerResult{true}; } if (decrypt_status == SbDrmSystemPrivate::kFailure) { - return {false, "Sample decryption failure."}; + return HandlerResult{false, "Sample decryption failure."}; } } DumpInputHash(input_buffer); @@ -268,19 +268,19 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( SB_DCHECK(input_buffers.front()->sample_type() == kSbMediaTypeVideo); if (!video_renderer_) { - return {false, "Invalid video renderer."}; + return HandlerResult{false, "Invalid video renderer."}; } if (video_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write video sample after EOS is reached"; } else { if (!video_renderer_->CanAcceptMoreData()) { - return {true}; + return HandlerResult{true}; } for (const auto& input_buffer : input_buffers) { if (input_buffer->drm_info()) { if (!SbDrmSystemIsValid(drm_system_)) { - return {false, "Invalid DRM system."}; + return HandlerResult{false, "Invalid DRM system."}; } DumpInputHash(input_buffer); SbDrmSystemPrivate::DecryptStatus decrypt_status = @@ -291,10 +291,10 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( InputBuffers(input_buffers.begin(), input_buffers.begin() + *samples_written)); } - return {true}; + return HandlerResult{true}; } if (decrypt_status == SbDrmSystemPrivate::kFailure) { - return {false, "Sample decryption failure."}; + return HandlerResult{false, "Sample decryption failure."}; } } DumpInputHash(input_buffer); @@ -304,7 +304,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteSamples( } } - return {true}; + return HandlerResult{true}; } HandlerResult FilterBasedPlayerWorkerHandler::WriteEndOfStream( @@ -314,7 +314,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteEndOfStream( if (sample_type == kSbMediaTypeAudio) { if (!audio_renderer_) { SB_LOG(INFO) << "Audio EOS enqueued when renderer is NULL."; - return {false, "Audio EOS enqueued when renderer is NULL."}; + return HandlerResult{false, "Audio EOS enqueued when renderer is NULL."}; } if (audio_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write audio EOS after EOS is enqueued"; @@ -325,7 +325,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteEndOfStream( } else { if (!video_renderer_) { SB_LOG(INFO) << "Video EOS enqueued when renderer is NULL."; - return {false, "Video EOS enqueued when renderer is NULL."}; + return HandlerResult{false, "Video EOS enqueued when renderer is NULL."}; } if (video_renderer_->IsEndOfStreamWritten()) { SB_LOG(WARNING) << "Try to write video EOS after EOS is enqueued"; @@ -335,7 +335,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::WriteEndOfStream( } } - return {true}; + return HandlerResult{true}; } HandlerResult FilterBasedPlayerWorkerHandler::SetPause(bool pause) { @@ -345,7 +345,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::SetPause(bool pause) { << ", and media time provider is " << media_time_provider_; if (!media_time_provider_) { - return {false, "Invalid media time provider."}; + return HandlerResult{false, "Invalid media time provider."}; } paused_ = pause; @@ -356,7 +356,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::SetPause(bool pause) { media_time_provider_->Play(); } Update(); - return {true}; + return HandlerResult{true}; } HandlerResult FilterBasedPlayerWorkerHandler::SetPlaybackRate( @@ -370,12 +370,12 @@ HandlerResult FilterBasedPlayerWorkerHandler::SetPlaybackRate( playback_rate_ = playback_rate; if (!media_time_provider_) { - return {false, "Invalid media time provider."}; + return HandlerResult{false, "Invalid media time provider."}; } media_time_provider_->SetPlaybackRate(playback_rate_); Update(); - return {true}; + return HandlerResult{true}; } void FilterBasedPlayerWorkerHandler::SetVolume(double volume) { @@ -413,7 +413,7 @@ HandlerResult FilterBasedPlayerWorkerHandler::SetBounds(const Bounds& bounds) { } } - return {true}; + return HandlerResult{true}; } void FilterBasedPlayerWorkerHandler::OnError(SbPlayerError error, diff --git a/starboard/shared/starboard/player/player_worker.cc b/starboard/shared/starboard/player/player_worker.cc index b07f41574d70..09947e895f9d 100644 --- a/starboard/shared/starboard/player/player_worker.cc +++ b/starboard/shared/starboard/player/player_worker.cc @@ -161,9 +161,16 @@ void PlayerWorker::UpdatePlayerState(SbPlayerState player_state) { } void PlayerWorker::UpdatePlayerError(SbPlayerError error, + HandlerResult result, const std::string& error_message) { + SB_DCHECK(!result.success); + std::string complete_error_message = error_message; + if (!result.error_message.empty()) { + complete_error_message += " Error: " + result.error_message; + } + SB_LOG(WARNING) << "Encountered player error " << error - << " with message: " << error_message; + << " with message: " << complete_error_message; // Only report the first error. if (error_occurred_.exchange(true)) { return; @@ -171,7 +178,7 @@ void PlayerWorker::UpdatePlayerError(SbPlayerError error, if (!player_error_func_) { return; } - player_error_func_(player_, context_, error, error_message.c_str()); + player_error_func_(player_, context_, error, complete_error_message.c_str()); } // static @@ -200,8 +207,8 @@ void PlayerWorker::DoInit() { SB_DCHECK(job_queue_->BelongsToCurrentThread()); Handler::UpdatePlayerErrorCB update_player_error_cb; - update_player_error_cb = - std::bind(&PlayerWorker::UpdatePlayerError, this, _1, _2); + update_player_error_cb = std::bind(&PlayerWorker::UpdatePlayerError, this, _1, + HandlerResult{false}, _2); HandlerResult result = handler_->Init( player_, std::bind(&PlayerWorker::UpdateMediaInfo, this, _1, _2, _3), std::bind(&PlayerWorker::player_state, this), @@ -210,11 +217,8 @@ void PlayerWorker::DoInit() { if (result.success) { UpdatePlayerState(kSbPlayerStateInitialized); } else { - std::string error_message = "Failed to initialize PlayerWorker."; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, error_message); + UpdatePlayerError(kSbPlayerErrorDecode, result, + "Failed to initialize PlayerWorker."); } } @@ -241,11 +245,7 @@ void PlayerWorker::DoSeek(SbTime seek_to_time, int ticket) { HandlerResult result = handler_->Seek(seek_to_time, ticket); if (!result.success) { - std::string error_message = "Failed seek."; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, error_message); + UpdatePlayerError(kSbPlayerErrorDecode, result, "Failed seek."); return; } @@ -288,11 +288,7 @@ void PlayerWorker::DoWriteSamples(InputBuffers input_buffers) { HandlerResult result = handler_->WriteSamples(input_buffers, &samples_written); if (!result.success) { - std::string error_message = "Failed to write sample."; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, error_message); + UpdatePlayerError(kSbPlayerErrorDecode, result, "Failed to write sample."); return; } if (samples_written == input_buffers.size()) { @@ -360,11 +356,8 @@ void PlayerWorker::DoWriteEndOfStream(SbMediaType sample_type) { HandlerResult result = handler_->WriteEndOfStream(sample_type); if (!result.success) { - std::string error_message = "Failed to write end of stream."; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, error_message); + UpdatePlayerError(kSbPlayerErrorDecode, result, + "Failed to write end of stream."); } } @@ -372,11 +365,7 @@ void PlayerWorker::DoSetBounds(Bounds bounds) { SB_DCHECK(job_queue_->BelongsToCurrentThread()); HandlerResult result = handler_->SetBounds(bounds); if (!result.success) { - std::string error_message = "Failed to set bounds"; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, "Failed to set bounds"); + UpdatePlayerError(kSbPlayerErrorDecode, result, "Failed to set bounds."); } } @@ -385,11 +374,7 @@ void PlayerWorker::DoSetPause(bool pause) { HandlerResult result = handler_->SetPause(pause); if (!result.success) { - std::string error_message = "Failed to set pause."; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, error_message); + UpdatePlayerError(kSbPlayerErrorDecode, result, "Failed to set pause."); } } @@ -398,11 +383,8 @@ void PlayerWorker::DoSetPlaybackRate(double playback_rate) { HandlerResult result = handler_->SetPlaybackRate(playback_rate); if (!result.success) { - std::string error_message = "Failed to set playback rate."; - if (!result.error_message.empty()) { - error_message += " Error: " + result.error_message; - } - UpdatePlayerError(kSbPlayerErrorDecode, "Failed to set playback rate."); + UpdatePlayerError(kSbPlayerErrorDecode, result, + "Failed to set playback rate."); } } diff --git a/starboard/shared/starboard/player/player_worker.h b/starboard/shared/starboard/player/player_worker.h index 805ddd382900..96bae3d002cf 100644 --- a/starboard/shared/starboard/player/player_worker.h +++ b/starboard/shared/starboard/player/player_worker.h @@ -63,6 +63,13 @@ class PlayerWorker { // All functions of this class will be called from the JobQueue thread. class Handler { public: + // Stores the success status of Handler operations. If |success| is false, + // |error_message| may be set with details of the error. + struct HandlerResult { + bool success; + std::string error_message; + }; + typedef PlayerWorker::Bounds Bounds; typedef ::starboard::shared::starboard::player::InputBuffer InputBuffer; typedef ::starboard::shared::starboard::player::InputBuffers InputBuffers; @@ -76,13 +83,6 @@ class PlayerWorker { const std::string& error_message)> UpdatePlayerErrorCB; - // Stores the success status of Handler operations. If |success| is false, - // |error_message| may be set with details of the error. - typedef struct HandlerResult { - bool success; - std::string error_message; - } HandlerResult; - Handler() = default; virtual ~Handler() {} @@ -195,7 +195,9 @@ class PlayerWorker { SbPlayerState player_state() const { return player_state_; } void UpdatePlayerState(SbPlayerState player_state); - void UpdatePlayerError(SbPlayerError error, const std::string& message); + void UpdatePlayerError(SbPlayerError error, + Handler::HandlerResult result, + const std::string& message); static void* ThreadEntryPoint(void* context); void RunLoop(); From ee8fecb3769ea490d7e9b869a71045d9c97f6b21 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 25 Sep 2023 12:02:22 -0700 Subject: [PATCH 059/140] Cherry pick PR #1444: Media Metrics Provider - Start making use of Cobalt Telemetry pipeline (#1507) Refer to the original PR: https://github.com/youtube/cobalt/pull/1444 Adding a new MediaMetricsProvider, based upon Chromium's. https://source.chromium.org/chromium/chromium/src/+/main:media/mojo/services/media_metrics_provider.h It is owned by web_media_player_impl and passed into sbplayer_pipeline. It's state is updated several times during video playback, and upon destruction of the web_media_player_impl, will send UMA histograms of the media pipeline state, including Audio and Video codecs used, and last known state. b/287670693 Co-authored-by: thorsten sideb0ard --- cobalt/media/BUILD.gn | 2 + cobalt/media/base/metrics_provider.cc | 126 ++++++++++++++++++ cobalt/media/base/metrics_provider.h | 91 +++++++++++++ cobalt/media/base/sbplayer_pipeline.cc | 6 +- cobalt/media/base/sbplayer_pipeline.h | 6 +- cobalt/media/player/web_media_player_impl.cc | 9 +- cobalt/media/player/web_media_player_impl.h | 3 + .../histograms/metadata/cobalt/enums.xml | 32 +++++ .../histograms/metadata/cobalt/histograms.xml | 98 +++++++++++++- 9 files changed, 369 insertions(+), 4 deletions(-) create mode 100644 cobalt/media/base/metrics_provider.cc create mode 100644 cobalt/media/base/metrics_provider.h diff --git a/cobalt/media/BUILD.gn b/cobalt/media/BUILD.gn index 0e7c06dda175..bde4beba217e 100644 --- a/cobalt/media/BUILD.gn +++ b/cobalt/media/BUILD.gn @@ -43,6 +43,8 @@ component("media") { "base/format_support_query_metrics.h", "base/interleaved_sinc_resampler.cc", "base/interleaved_sinc_resampler.h", + "base/metrics_provider.cc", + "base/metrics_provider.h", "base/playback_statistics.cc", "base/playback_statistics.h", "base/sbplayer_bridge.cc", diff --git a/cobalt/media/base/metrics_provider.cc b/cobalt/media/base/metrics_provider.cc new file mode 100644 index 000000000000..de9d41e39e95 --- /dev/null +++ b/cobalt/media/base/metrics_provider.cc @@ -0,0 +1,126 @@ +// Copyright 2023 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. + +#include "cobalt/media/base/metrics_provider.h" + +#include "base/logging.h" +#include "base/metrics/histogram_functions.h" + +namespace cobalt { +namespace media { + +using starboard::ScopedLock; + +MediaMetricsProvider::~MediaMetricsProvider() { + if (!IsInitialized()) return; + ReportPipelineUMA(); +} + +void MediaMetricsProvider::Initialize(bool is_mse) { + if (IsInitialized()) { + return; + } + + ScopedLock scoped_lock(mutex_); + media_info_.emplace(MediaInfo(is_mse)); +} + +void MediaMetricsProvider::OnError(const ::media::PipelineStatus status) { + DCHECK(IsInitialized()); + ScopedLock scoped_lock(mutex_); + uma_info_.last_pipeline_status = status; +} + +void MediaMetricsProvider::SetHasAudio(AudioCodec audio_codec) { + ScopedLock scoped_lock(mutex_); + uma_info_.audio_codec = audio_codec; + uma_info_.has_audio = true; +} + +void MediaMetricsProvider::SetHasVideo(VideoCodec video_codec) { + ScopedLock scoped_lock(mutex_); + uma_info_.video_codec = video_codec; + uma_info_.has_video = true; +} + +void MediaMetricsProvider::SetHasPlayed() { + ScopedLock scoped_lock(mutex_); + uma_info_.has_ever_played = true; +} + +void MediaMetricsProvider::SetHaveEnough() { + ScopedLock scoped_lock(mutex_); + uma_info_.has_reached_have_enough = true; +} + +void MediaMetricsProvider::SetIsEME() { + ScopedLock scoped_lock(mutex_); + // This may be called before Initialize(). + uma_info_.is_eme = true; +} + +void MediaMetricsProvider::ReportPipelineUMA() { + ScopedLock scoped_lock(mutex_); + if (uma_info_.has_video && uma_info_.has_audio) { + base::UmaHistogramEnumeration(GetUMANameForAVStream(uma_info_), + uma_info_.last_pipeline_status, + PipelineStatus::PIPELINE_STATUS_MAX); + } else if (uma_info_.has_audio) { + base::UmaHistogramEnumeration("Cobalt.Media.PipelineStatus.AudioOnly", + uma_info_.last_pipeline_status, + PipelineStatus::PIPELINE_STATUS_MAX); + } else if (uma_info_.has_video) { + base::UmaHistogramEnumeration("Cobalt.Media.PipelineStatus.VideoOnly", + uma_info_.last_pipeline_status, + PipelineStatus::PIPELINE_STATUS_MAX); + } else { + // Note: This metric can be recorded as a result of normal operation with + // Media Source Extensions. If a site creates a MediaSource object but never + // creates a source buffer or appends data, PIPELINE_OK will be recorded. + base::UmaHistogramEnumeration("Cobalt.Media.PipelineStatus.Unsupported", + uma_info_.last_pipeline_status, + PipelineStatus::PIPELINE_STATUS_MAX); + } + + // Report whether this player ever saw a playback event. Used to measure the + // effectiveness of efforts to reduce loaded-but-never-used players. + if (uma_info_.has_reached_have_enough) + base::UmaHistogramBoolean("Cobalt.Media.HasEverPlayed", + uma_info_.has_ever_played); +} + +std::string MediaMetricsProvider::GetUMANameForAVStream( + const PipelineInfo& player_info) const { + constexpr char kPipelineUmaPrefix[] = + "Cobalt.Media.PipelineStatus.AudioVideo."; + std::string uma_name = kPipelineUmaPrefix; + if (player_info.video_codec == VideoCodec::kVP9) + uma_name += "VP9"; + else if (player_info.video_codec == VideoCodec::kH264) + uma_name += "H264"; + else if (player_info.video_codec == VideoCodec::kAV1) + uma_name += "AV1"; + else + uma_name += "Other"; + + return uma_name; +} + +bool MediaMetricsProvider::IsInitialized() const { + ScopedLock scoped_lock(mutex_); + return media_info_.has_value(); +} + +} // namespace media +} // namespace cobalt diff --git a/cobalt/media/base/metrics_provider.h b/cobalt/media/base/metrics_provider.h new file mode 100644 index 000000000000..dff1df7f3ae4 --- /dev/null +++ b/cobalt/media/base/metrics_provider.h @@ -0,0 +1,91 @@ +// Copyright 2023 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 COBALT_MEDIA_BASE_METRICS_PROVIDER_H_ +#define COBALT_MEDIA_BASE_METRICS_PROVIDER_H_ + +#include + +#include "starboard/common/mutex.h" +#include "third_party/chromium/media/base/audio_codecs.h" +#include "third_party/chromium/media/base/container_names.h" +#include "third_party/chromium/media/base/pipeline_status.h" +#include "third_party/chromium/media/base/timestamp_constants.h" +#include "third_party/chromium/media/base/video_codecs.h" + +namespace cobalt { +namespace media { + +using AudioCodec = ::media::AudioCodec; +using VideoCodec = ::media::VideoCodec; +using PipelineStatus = ::media::PipelineStatus; +using VideoDecoderType = ::media::VideoDecoderType; + +class MediaMetricsProvider { + public: + MediaMetricsProvider() = default; + ~MediaMetricsProvider(); + + private: + struct PipelineInfo { + PipelineInfo() = default; + ~PipelineInfo() = default; + bool has_ever_played = false; + bool has_reached_have_enough = false; + bool has_audio = false; + bool has_video = false; + bool is_eme = false; + bool video_decoder_changed = false; + ::media::AudioCodec audio_codec; + ::media::VideoCodec video_codec; + ::media::PipelineStatus last_pipeline_status = + ::media::PipelineStatus::PIPELINE_OK; + }; + + struct MediaInfo { + explicit MediaInfo(bool is_mse) : is_mse{is_mse} {}; + const bool is_mse; + }; + + + public: + // based on mojom::MediaMetricsProvider + void Initialize(bool is_mse); + void OnError(const ::media::PipelineStatus status); + void SetHasAudio(::media::AudioCodec audio_codec); + void SetHasVideo(::media::VideoCodec video_codec); + void SetHasPlayed(); + void SetHaveEnough(); + void SetIsEME(); + + void ReportPipelineUMA(); + + private: + std::string GetUMANameForAVStream(const PipelineInfo& player_info) const; + bool IsInitialized() const; + + private: + // UMA pipeline packaged data + PipelineInfo uma_info_; + + // The values below are only set if `Initialize` has been called. + absl::optional media_info_; + + starboard::Mutex mutex_; +}; + +} // namespace media +} // namespace cobalt + +#endif // COBALT_MEDIA_BASE_METRICS_PROVIDER_H_ diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc index c79ead5950c2..8280e53fc685 100644 --- a/cobalt/media/base/sbplayer_pipeline.cc +++ b/cobalt/media/base/sbplayer_pipeline.cc @@ -111,7 +111,8 @@ SbPlayerPipeline::SbPlayerPipeline( #if SB_API_VERSION >= 15 SbTime audio_write_duration_local, SbTime audio_write_duration_remote, #endif // SB_API_VERSION >= 15 - MediaLog* media_log, DecodeTargetProvider* decode_target_provider) + MediaLog* media_log, MediaMetricsProvider* media_metrics_provider, + DecodeTargetProvider* decode_target_provider) : pipeline_identifier_( base::StringPrintf("%X", g_pipeline_identifier_counter++)), sbplayer_interface_(interface), @@ -156,6 +157,7 @@ SbPlayerPipeline::SbPlayerPipeline( audio_write_duration_local_(audio_write_duration_local), audio_write_duration_remote_(audio_write_duration_remote), #endif // SB_API_VERSION >= 15 + media_metrics_provider_(media_metrics_provider), last_media_time_(base::StringPrintf("Media.Pipeline.%s.LastMediaTime", pipeline_identifier_.c_str()), 0, "Last media time reported by the underlying player."), @@ -1244,10 +1246,12 @@ void SbPlayerPipeline::UpdateDecoderConfig(DemuxerStream* stream) { if (stream->type() == DemuxerStream::AUDIO) { const AudioDecoderConfig& decoder_config = stream->audio_decoder_config(); + media_metrics_provider_->SetHasAudio(decoder_config.codec()); player_bridge_->UpdateAudioConfig(decoder_config, stream->mime_type()); } else { DCHECK_EQ(stream->type(), DemuxerStream::VIDEO); const VideoDecoderConfig& decoder_config = stream->video_decoder_config(); + media_metrics_provider_->SetHasVideo(decoder_config.codec()); base::AutoLock auto_lock(lock_); bool natural_size_changed = (decoder_config.natural_size().width() != natural_size_.width() || diff --git a/cobalt/media/base/sbplayer_pipeline.h b/cobalt/media/base/sbplayer_pipeline.h index 59e19e57efa4..dc851c818d4a 100644 --- a/cobalt/media/base/sbplayer_pipeline.h +++ b/cobalt/media/base/sbplayer_pipeline.h @@ -28,6 +28,7 @@ #include "cobalt/base/c_val.h" #include "cobalt/math/size.h" #include "cobalt/media/base/media_export.h" +#include "cobalt/media/base/metrics_provider.h" #include "cobalt/media/base/pipeline.h" #include "cobalt/media/base/playback_statistics.h" #include "cobalt/media/base/sbplayer_bridge.h" @@ -65,7 +66,8 @@ class MEDIA_EXPORT SbPlayerPipeline : public Pipeline, #if SB_API_VERSION >= 15 SbTime audio_write_duration_local, SbTime audio_write_duration_remote, #endif // SB_API_VERSION >= 15 - MediaLog* media_log, DecodeTargetProvider* decode_target_provider); + MediaLog* media_log, MediaMetricsProvider* media_metrics_provider, + DecodeTargetProvider* decode_target_provider); ~SbPlayerPipeline() override; void Suspend() override; @@ -305,6 +307,8 @@ class MEDIA_EXPORT SbPlayerPipeline : public Pipeline, base::CVal ended_; base::CVal player_state_; + MediaMetricsProvider* media_metrics_provider_; + DecodeTargetProvider* decode_target_provider_; #if SB_API_VERSION >= 15 diff --git a/cobalt/media/player/web_media_player_impl.cc b/cobalt/media/player/web_media_player_impl.cc index 8a121a647adb..6941c04d3d83 100644 --- a/cobalt/media/player/web_media_player_impl.cc +++ b/cobalt/media/player/web_media_player_impl.cc @@ -156,7 +156,7 @@ WebMediaPlayerImpl::WebMediaPlayerImpl( #if SB_API_VERSION >= 15 audio_write_duration_local, audio_write_duration_remote, #endif // SB_API_VERSION >= 15 - media_log_, decode_target_provider_.get()); + media_log_, &media_metrics_provider_, decode_target_provider_.get()); // Also we want to be notified of |main_loop_| destruction. main_loop_->AddDestructionObserver(this); @@ -244,6 +244,7 @@ void WebMediaPlayerImpl::LoadUrl(const GURL& url) { is_local_source_ = !url.SchemeIs("http") && !url.SchemeIs("https"); StartPipeline(url); + media_metrics_provider_.Initialize(false); } #endif // SB_HAS(PLAYER_WITH_URL) @@ -270,6 +271,7 @@ void WebMediaPlayerImpl::LoadMediaSource() { state_.is_media_source = true; StartPipeline(chunk_demuxer_.get()); + media_metrics_provider_.Initialize(true); } void WebMediaPlayerImpl::LoadProgressive( @@ -309,6 +311,7 @@ void WebMediaPlayerImpl::LoadProgressive( state_.is_progressive = true; StartPipeline(progressive_demuxer_.get()); + media_metrics_provider_.Initialize(false); } void WebMediaPlayerImpl::CancelLoad() { @@ -324,6 +327,7 @@ void WebMediaPlayerImpl::Play() { pipeline_->SetPlaybackRate(state_.playback_rate); media_log_->AddEvent<::media::MediaLogEvent::kPlay>(); + media_metrics_provider_.SetHasPlayed(); } void WebMediaPlayerImpl::Pause() { @@ -653,6 +657,7 @@ void WebMediaPlayerImpl::OnPipelineError(::media::PipelineStatus error, if (suppress_destruction_errors_) return; media_log_->NotifyError(error); + media_metrics_provider_.OnError(error); if (ready_state_ == WebMediaPlayer::kReadyStateHaveNothing) { // Any error that occurs before reaching ReadyStateHaveMetadata should @@ -785,6 +790,7 @@ void WebMediaPlayerImpl::OnPipelineBufferingState( break; case Pipeline::kPrerollCompleted: SetReadyState(WebMediaPlayer::kReadyStateHaveEnoughData); + media_metrics_provider_.SetHaveEnough(); break; } } @@ -963,6 +969,7 @@ void WebMediaPlayerImpl::OnEncryptedMediaInitDataEncounteredWrapper( NOTREACHED(); break; } + media_metrics_provider_.SetIsEME(); } WebMediaPlayerClient* WebMediaPlayerImpl::GetClient() { diff --git a/cobalt/media/player/web_media_player_impl.h b/cobalt/media/player/web_media_player_impl.h index 9db4419ecdcd..adb7ed2a577b 100644 --- a/cobalt/media/player/web_media_player_impl.h +++ b/cobalt/media/player/web_media_player_impl.h @@ -60,6 +60,7 @@ #include "base/time/time.h" #include "cobalt/math/size.h" #include "cobalt/media/base/decode_target_provider.h" +#include "cobalt/media/base/metrics_provider.h" #include "cobalt/media/base/pipeline.h" #include "cobalt/media/base/sbplayer_interface.h" #include "cobalt/media/player/web_media_player.h" @@ -306,6 +307,8 @@ class WebMediaPlayerImpl : public WebMediaPlayer, ::media::MediaLog* const media_log_; + MediaMetricsProvider media_metrics_provider_; + bool is_local_source_; std::unique_ptr<::media::Demuxer> progressive_demuxer_; diff --git a/tools/metrics/histograms/metadata/cobalt/enums.xml b/tools/metrics/histograms/metadata/cobalt/enums.xml index 6888e1b7cfbe..feb372a0520c 100644 --- a/tools/metrics/histograms/metadata/cobalt/enums.xml +++ b/tools/metrics/histograms/metadata/cobalt/enums.xml @@ -37,6 +37,38 @@ https://chromium.googlesource.com/chromium/src.git/+/HEAD/tools/metrics/histogra + + + + Possible status values reported by the Media Pipeline + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tools/metrics/histograms/metadata/cobalt/histograms.xml b/tools/metrics/histograms/metadata/cobalt/histograms.xml index e958d8c1b5ec..467804c88ac2 100644 --- a/tools/metrics/histograms/metadata/cobalt/histograms.xml +++ b/tools/metrics/histograms/metadata/cobalt/histograms.xml @@ -55,6 +55,102 @@ Always run the pretty print utility on this file after editing: + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio-video + streams with an unknown video codec. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio only + streams. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio-video + streams with AV1 video codec. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio-video + streams with H264 video codec. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio-video + streams with an unknown video codec. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio-video + streams with VP9 video codec. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for audio-video + streams with an unknown video codec. + + + + + + + sideboard@google.com + cobalt-team@google.com + + Status of the media pipeline at the end of its lifecycle for video only + streams. + + + - + \ No newline at end of file From faede4f6a900c25c5fd0da1c727437cd31023eb5 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 25 Sep 2023 15:09:42 -0700 Subject: [PATCH 060/140] Cherry pick PR #1607: [android] Add SB version guards (#1632) Refer to the original PR: https://github.com/youtube/cobalt/pull/1607 b/284140486 Co-authored-by: Jason --- starboard/android/shared/media_capabilities_cache.cc | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/starboard/android/shared/media_capabilities_cache.cc b/starboard/android/shared/media_capabilities_cache.cc index 0d508243713a..2d07a7b061dd 100644 --- a/starboard/android/shared/media_capabilities_cache.cc +++ b/starboard/android/shared/media_capabilities_cache.cc @@ -74,6 +74,7 @@ constexpr int TYPE_USB_HEADSET = 22; constexpr int TYPE_WIRED_HEADPHONES = 4; constexpr int TYPE_WIRED_HEADSET = 3; +#if SB_API_VERSION >= 15 SbMediaAudioConnector GetConnectorFromAndroidOutputType( int android_output_device_type) { switch (android_output_device_type) { @@ -145,6 +146,7 @@ SbMediaAudioConnector GetConnectorFromAndroidOutputType( << android_output_device_type; return kSbMediaAudioConnectorUnknown; } +#endif // SB_API_VERSION >= 15 bool EndsWith(const std::string& str, const std::string& suffix) { if (str.size() < suffix.size()) { @@ -267,8 +269,12 @@ bool GetAudioConfiguration(int index, return env->CallIntMethodOrAbort(j_output_device_info.Get(), name, "()I"); }; +#if SB_API_VERSION >= 15 configuration->connector = GetConnectorFromAndroidOutputType(call_int_method("getType")); +#else // SB_API_VERSION >= 15 + configuration->connector = kSbMediaAudioConnectorHdmi; +#endif // SB_API_VERSION >= 15 configuration->latency = 0; configuration->coding_type = kSbMediaAudioCodingTypePcm; configuration->number_of_channels = call_int_method("getChannels"); From ead474da80a873d86075f8dac50bad1bac29109f Mon Sep 17 00:00:00 2001 From: Kaido Kert Date: Tue, 26 Sep 2023 14:03:56 -0700 Subject: [PATCH 061/140] Update LTS minor version to 12 (#1641) b/260110906 --- cobalt/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/version.h b/cobalt/version.h index 284be91a424e..cdce9e2579c6 100644 --- a/cobalt/version.h +++ b/cobalt/version.h @@ -35,6 +35,6 @@ // release is cut. //. -#define COBALT_VERSION "24.lts.11" +#define COBALT_VERSION "24.lts.12" #endif // COBALT_VERSION_H_ From 1f6f9a61db4a91b978eed3f8d33c89957b30ca90 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 26 Sep 2023 18:31:11 -0700 Subject: [PATCH 062/140] Cherry pick PR #1327: Refactor persistent settings for Cobalt cache (#1642) Refer to the original PR: https://github.com/youtube/cobalt/pull/1327 Move persistent settings module ownership to URLRequestContext, which also owns the cache and cache backends. This allows removing reverse depenendencies from /net code to /cobalt/persistent_settings, while leaving runtime functionality the same. Note: This intentionally just moves the code from `CobaltBackendImpl` to `URLRequestContext` with minimal changes. A future refactor could encapsulate the settings into its own class. b/296578318 Co-authored-by: Kaido Kert --- cobalt/browser/browser_module.cc | 6 +- cobalt/h5vcc/h5vcc_storage.cc | 5 +- cobalt/network/url_request_context.cc | 60 +++++++++++++++++++- cobalt/network/url_request_context.h | 9 +++ net/BUILD.gn | 1 - net/base/cache_type.h | 3 - net/disk_cache/cobalt/cobalt_backend_impl.cc | 53 ++--------------- net/disk_cache/cobalt/cobalt_backend_impl.h | 7 +-- 8 files changed, 78 insertions(+), 66 deletions(-) diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc index 85d5d93680c6..abe8c4298521 100644 --- a/cobalt/browser/browser_module.cc +++ b/cobalt/browser/browser_module.cc @@ -2234,11 +2234,7 @@ void BrowserModule::ValidateCacheBackendSettings() { auto url_request_context = network_module_->url_request_context(); auto http_cache = url_request_context->http_transaction_factory()->GetCache(); if (!http_cache) return; - auto cache_backend = static_cast( - http_cache->GetCurrentBackend()); - if (cache_backend) { - cache_backend->ValidatePersistentSettings(); - } + network_module_->url_request_context()->ValidateCachePersistentSettings(); } } // namespace browser diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc index dac5b5c1ed85..ebbd3060ac0d 100644 --- a/cobalt/h5vcc/h5vcc_storage.cc +++ b/cobalt/h5vcc/h5vcc_storage.cc @@ -314,7 +314,10 @@ H5vccStorageSetQuotaResponse H5vccStorage::SetQuota( void H5vccStorage::SetAndSaveQuotaForBackend(disk_cache::ResourceType type, uint32_t bytes) { if (cache_backend_) { - cache_backend_->UpdateSizes(type, bytes); + if (cache_backend_->UpdateSizes(type, bytes)) { + auto url_request_context = network_module_->url_request_context(); + url_request_context->UpdateCacheSizeSetting(type, bytes); + } if (bytes == 0) { network_module_->task_runner()->PostTask( diff --git a/cobalt/network/url_request_context.cc b/cobalt/network/url_request_context.cc index 7467b9841016..219d97af2899 100644 --- a/cobalt/network/url_request_context.cc +++ b/cobalt/network/url_request_context.cc @@ -14,6 +14,7 @@ #include "cobalt/network/url_request_context.h" +#include #include #include #include @@ -48,6 +49,45 @@ #include "net/url_request/data_protocol_handler.h" #include "net/url_request/url_request_job_factory_impl.h" +namespace { + +const char kPersistentSettingsJson[] = "cache_settings.json"; + + +void ReadDiskCacheSize(cobalt::persistent_storage::PersistentSettings* settings, + int64_t max_bytes) { + auto total_size = 0; + disk_cache::ResourceTypeMetadata kTypeMetadataNew[disk_cache::kTypeCount]; + + for (int i = 0; i < disk_cache::kTypeCount; i++) { + auto metadata = disk_cache::kTypeMetadata[i]; + uint32_t bucket_size = + static_cast(settings->GetPersistentSettingAsDouble( + metadata.directory, metadata.max_size_bytes)); + kTypeMetadataNew[i] = {metadata.directory, bucket_size}; + + total_size += bucket_size; + } + + // Check if PersistentSettings values are valid and can replace the + // disk_cache::kTypeMetadata. + if (total_size <= max_bytes) { + std::copy(std::begin(kTypeMetadataNew), std::end(kTypeMetadataNew), + std::begin(disk_cache::kTypeMetadata)); + return; + } + + // PersistentSettings values are invalid and will be replaced by the default + // values in disk_cache::kTypeMetadata. + for (int i = 0; i < disk_cache::kTypeCount; i++) { + auto metadata = disk_cache::kTypeMetadata[i]; + settings->SetPersistentSetting( + metadata.directory, std::make_unique( + static_cast(metadata.max_size_bytes))); + } +} +} // namespace + namespace cobalt { namespace network { namespace { @@ -189,10 +229,16 @@ URLRequestContext::URLRequestContext( // is less than 1 mb and subtract this from the max_cache_bytes. max_cache_bytes -= (1 << 20); + // Initialize and read caching persistent settings + cache_persistent_settings_ = + std::make_unique( + kPersistentSettingsJson); + ReadDiskCacheSize(cache_persistent_settings_.get(), max_cache_bytes); + auto http_cache = std::make_unique( storage_.http_network_session(), std::make_unique( - net::DISK_CACHE, net::CACHE_BACKEND_COBALT, + net::DISK_CACHE, net::CACHE_BACKEND_DEFAULT, base::FilePath(std::string(path.data())), /* max_bytes */ max_cache_bytes), true); @@ -243,5 +289,17 @@ void URLRequestContext::OnQuicToggle(const std::string& message) { } #endif // defined(ENABLE_DEBUGGER) +void URLRequestContext::UpdateCacheSizeSetting(disk_cache::ResourceType type, + uint32_t bytes) { + CHECK(cache_persistent_settings_); + cache_persistent_settings_->SetPersistentSetting( + disk_cache::kTypeMetadata[type].directory, + std::make_unique(static_cast(bytes))); +} + +void URLRequestContext::ValidateCachePersistentSettings() { + cache_persistent_settings_->ValidatePersistentSettings(); +} + } // namespace network } // namespace cobalt diff --git a/cobalt/network/url_request_context.h b/cobalt/network/url_request_context.h index 6e8047965708..a8e635cd9b4a 100644 --- a/cobalt/network/url_request_context.h +++ b/cobalt/network/url_request_context.h @@ -15,6 +15,7 @@ #ifndef COBALT_NETWORK_URL_REQUEST_CONTEXT_H_ #define COBALT_NETWORK_URL_REQUEST_CONTEXT_H_ +#include #include #include "base/basictypes.h" @@ -22,6 +23,7 @@ #include "base/sequence_checker.h" #include "cobalt/persistent_storage/persistent_settings.h" #include "net/cookies/cookie_monster.h" +#include "net/disk_cache/cobalt/resource_type.h" #include "net/log/net_log.h" #include "net/url_request/url_request_context.h" #include "net/url_request/url_request_context_getter.h" @@ -53,6 +55,9 @@ class URLRequestContext : public net::URLRequestContext { bool using_http_cache(); + void UpdateCacheSizeSetting(disk_cache::ResourceType type, uint32_t bytes); + void ValidateCachePersistentSettings(); + private: SEQUENCE_CHECKER(sequence_checker_); net::URLRequestContextStorage storage_; @@ -70,6 +75,10 @@ class URLRequestContext : public net::URLRequestContext { void OnQuicToggle(const std::string&); #endif // defined(ENABLE_DEBUGGER) + // Persistent settings module for Cobalt disk cache quotas + std::unique_ptr + cache_persistent_settings_; + DISALLOW_COPY_AND_ASSIGN(URLRequestContext); }; diff --git a/net/BUILD.gn b/net/BUILD.gn index e69452386eb2..833e724c097a 100644 --- a/net/BUILD.gn +++ b/net/BUILD.gn @@ -4217,7 +4217,6 @@ target(gtest_target_type, "net_unittests") { "//base:i18n", "//base/test:test_support", "//base/third_party/dynamic_annotations", - "//cobalt/persistent_storage:persistent_settings", "//crypto", "//testing/gmock", "//testing/gtest", diff --git a/net/base/cache_type.h b/net/base/cache_type.h index debae5131639..c5e72efefec3 100644 --- a/net/base/cache_type.h +++ b/net/base/cache_type.h @@ -24,9 +24,6 @@ enum BackendType { CACHE_BACKEND_DEFAULT, CACHE_BACKEND_BLOCKFILE, // The |BackendImpl|. CACHE_BACKEND_SIMPLE, // The |SimpleBackendImpl|. -#if defined(STARBOARD) - CACHE_BACKEND_COBALT // The |CobaltBackendImpl|, -#endif }; } // namespace net diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.cc b/net/disk_cache/cobalt/cobalt_backend_impl.cc index b60ab38a7e81..6a2ec1c74874 100644 --- a/net/disk_cache/cobalt/cobalt_backend_impl.cc +++ b/net/disk_cache/cobalt/cobalt_backend_impl.cc @@ -30,8 +30,6 @@ namespace disk_cache { namespace { -const char kPersistentSettingsJson[] = "cache_settings.json"; - void CompletionOnceCallbackHandler( scoped_refptr runner, int result) { @@ -53,38 +51,6 @@ ResourceType GetType(const std::string& key) { return kOther; } -void ReadDiskCacheSize( - cobalt::persistent_storage::PersistentSettings* settings, - int64_t max_bytes) { - auto total_size = 0; - disk_cache::ResourceTypeMetadata kTypeMetadataNew[disk_cache::kTypeCount]; - - for (int i = 0; i < disk_cache::kTypeCount; i++) { - auto metadata = disk_cache::kTypeMetadata[i]; - uint32_t bucket_size = - static_cast(settings->GetPersistentSettingAsDouble( - metadata.directory, metadata.max_size_bytes)); - kTypeMetadataNew[i] = {metadata.directory, bucket_size}; - - total_size += bucket_size; - } - - // Check if PersistentSettings values are valid and can replace the disk_cache::kTypeMetadata. - if (total_size <= max_bytes) { - std::copy(std::begin(kTypeMetadataNew), std::end(kTypeMetadataNew), std::begin(disk_cache::kTypeMetadata)); - return; - } - - // PersistentSettings values are invalid and will be replaced by the default values in - // disk_cache::kTypeMetadata. - for (int i = 0; i < disk_cache::kTypeCount; i++) { - auto metadata = disk_cache::kTypeMetadata[i]; - settings->SetPersistentSetting( - metadata.directory, - std::make_unique(static_cast(metadata.max_size_bytes))); - } -} - } // namespace CobaltBackendImpl::CobaltBackendImpl( @@ -94,12 +60,9 @@ CobaltBackendImpl::CobaltBackendImpl( net::CacheType cache_type, net::NetLog* net_log) : weak_factory_(this) { - persistent_settings_ = - std::make_unique( - kPersistentSettingsJson); - ReadDiskCacheSize(persistent_settings_.get(), max_bytes); // Initialize disk backend for each resource type. + // Note: kTypeMetadata is refreshed from settings before this constructor runs int64_t total_size = 0; for (int i = 0; i < kTypeCount; i++) { auto metadata = kTypeMetadata[i]; @@ -123,28 +86,20 @@ CobaltBackendImpl::~CobaltBackendImpl() { simple_backend_map_.clear(); } -void CobaltBackendImpl::UpdateSizes(ResourceType type, uint32_t bytes) { +bool CobaltBackendImpl::UpdateSizes(ResourceType type, uint32_t bytes) { if (bytes == disk_cache::kTypeMetadata[type].max_size_bytes) - return; - - // Static cast value to double since base::Value cannot be a long. - persistent_settings_->SetPersistentSetting( - disk_cache::kTypeMetadata[type].directory, - std::make_unique(static_cast(bytes))); + return false; disk_cache::kTypeMetadata[type].max_size_bytes = bytes; SimpleBackendImpl* simple_backend = simple_backend_map_[type]; simple_backend->SetMaxSize(bytes); + return true; } uint32_t CobaltBackendImpl::GetQuota(ResourceType type) { return disk_cache::kTypeMetadata[type].max_size_bytes; } -void CobaltBackendImpl::ValidatePersistentSettings() { - persistent_settings_->ValidatePersistentSettings(); -} - net::Error CobaltBackendImpl::Init(CompletionOnceCallback completion_callback) { auto closure_runner = base::MakeRefCounted(std::move(completion_callback)); diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.h b/net/disk_cache/cobalt/cobalt_backend_impl.h index b3b1b5493d0c..effa70bd7d74 100644 --- a/net/disk_cache/cobalt/cobalt_backend_impl.h +++ b/net/disk_cache/cobalt/cobalt_backend_impl.h @@ -23,7 +23,6 @@ #include #include "base/callback_helpers.h" -#include "cobalt/persistent_storage/persistent_settings.h" #include "net/base/completion_once_callback.h" #include "net/disk_cache/cobalt/resource_type.h" #include "net/disk_cache/disk_cache.h" @@ -49,9 +48,8 @@ class NET_EXPORT_PRIVATE CobaltBackendImpl final : public Backend { ~CobaltBackendImpl() override; net::Error Init(CompletionOnceCallback completion_callback); - void UpdateSizes(ResourceType type, uint32_t bytes); + bool UpdateSizes(ResourceType type, uint32_t bytes); uint32_t GetQuota(ResourceType type); - void ValidatePersistentSettings(); // Backend interface. net::CacheType GetCacheType() const override; @@ -118,9 +116,6 @@ class NET_EXPORT_PRIVATE CobaltBackendImpl final : public Backend { std::map simple_backend_map_; - // Json PrefStore used for persistent settings. - std::unique_ptr - persistent_settings_; }; } // namespace disk_cache From cfc1c268c44b3dc823db479c9a6bf05cb1350264 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 27 Sep 2023 10:07:44 -0700 Subject: [PATCH 063/140] Cherry pick PR #1475: [XB1] Fix DX memory leak while suspend/resume (#1640) Refer to the original PR: https://github.com/youtube/cobalt/pull/1475 b/298967828 Change-Id: Ibbf739aac23401622b2751a0f0f3af3f169457db Co-authored-by: victorpasoshnikov --- .../shared/uwp/extended_resources_manager.cc | 22 +++++++++++-------- .../xb1/shared/gpu_base_video_decoder.cc | 6 +++++ starboard/xb1/shared/gpu_base_video_decoder.h | 1 + 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/starboard/shared/uwp/extended_resources_manager.cc b/starboard/shared/uwp/extended_resources_manager.cc index d7a82e013eb6..08f20da3c4ec 100644 --- a/starboard/shared/uwp/extended_resources_manager.cc +++ b/starboard/shared/uwp/extended_resources_manager.cc @@ -44,6 +44,7 @@ using Windows::Foundation::Metadata::ApiInformation; using ::starboard::xb1::shared::Av1VideoDecoder; using ::starboard::xb1::shared::VpxVideoDecoder; #endif // defined(INTERNAL_BUILD) +using ::starboard::xb1::shared::GpuVideoDecoderBase; const SbTime kReleaseTimeout = kSbTimeSecond; @@ -472,6 +473,8 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { } else { SB_LOG(INFO) << "CreateFence() failed with " << hr; } + // Clear frame buffers used for rendering queue + GpuVideoDecoderBase::ClearFrameBuffersPool(); } if (d3d12queue_) { @@ -483,15 +486,6 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { d3d12queue_.Reset(); } - if (d3d12device_) { -#if !defined(COBALT_BUILD_TYPE_GOLD) - d3d12device_->AddRef(); - ULONG reference_count = d3d12device_->Release(); - SB_LOG(INFO) << "Reference count of |d3d12device_| is " - << reference_count; -#endif - d3d12device_.Reset(); - } if (d3d12FrameBuffersHeap_) { #if !defined(COBALT_BUILD_TYPE_GOLD) d3d12FrameBuffersHeap_->AddRef(); @@ -502,6 +496,16 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { d3d12FrameBuffersHeap_.Reset(); } + if (d3d12device_) { +#if !defined(COBALT_BUILD_TYPE_GOLD) + d3d12device_->AddRef(); + ULONG reference_count = d3d12device_->Release(); + SB_LOG(INFO) << "Reference count of |d3d12device_| is " + << reference_count; +#endif + d3d12device_.Reset(); + } + } catch (const std::exception& e) { SB_LOG(ERROR) << "Exception on releasing extended resources: " << e.what(); OnNonrecoverableFailure(); diff --git a/starboard/xb1/shared/gpu_base_video_decoder.cc b/starboard/xb1/shared/gpu_base_video_decoder.cc index 62ff40229a1c..a37dc716f459 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.cc +++ b/starboard/xb1/shared/gpu_base_video_decoder.cc @@ -118,6 +118,8 @@ class GpuFrameBufferPool { return true; } + void Clear() { frame_buffers_.clear(); } + private: std::vector> frame_buffers_; @@ -754,6 +756,10 @@ void GpuVideoDecoderBase::ReleaseFrameBuffer(GpuFrameBuffer* frame_buffer) { frame_buffers_condition_.Signal(); } +void GpuVideoDecoderBase::ClearFrameBuffersPool() { + GetGpuFrameBufferPool()->Clear(); +} + GpuVideoDecoderBase::GpuFrameBuffer* GpuVideoDecoderBase::GetAvailableFrameBuffer(uint16_t width, uint16_t height) { if (decoder_behavior_.load() == kResettingDecoder) { diff --git a/starboard/xb1/shared/gpu_base_video_decoder.h b/starboard/xb1/shared/gpu_base_video_decoder.h index de0e45b4480f..1853a640fc1d 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.h +++ b/starboard/xb1/shared/gpu_base_video_decoder.h @@ -97,6 +97,7 @@ class GpuVideoDecoderBase int GetWidth() { return frame_width_; } int GetHeight() { return frame_height_; } bool IsHdrVideo() { return is_hdr_video_; } + static void ClearFrameBuffersPool(); protected: typedef ::starboard::shared::starboard::media::VideoStreamInfo From 7adb54ecf7cce246a647a671399dafb7ebd3239a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 27 Sep 2023 14:41:09 -0700 Subject: [PATCH 064/140] Cherry pick PR #1626: [XB1] Add backup version of win sdk (#1627) Refer to the original PR: https://github.com/youtube/cobalt/pull/1626 Adds check to see if the newer version of the win sdk is available before attempting to run makeappx and signtool, then falls back to 10.0.22000.0 if 10.0.22621.0 isn't found. b/299672207 Change-Id: I2e9ed373a940c30367aa095ff2a254ec5232dd83 Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/packager.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/starboard/xb1/tools/packager.py b/starboard/xb1/tools/packager.py index c2ab23dbae56..5d769a50b27b 100644 --- a/starboard/xb1/tools/packager.py +++ b/starboard/xb1/tools/packager.py @@ -53,6 +53,7 @@ } _DEFAULT_SDK_BIN_DIR = 'C:\\Program Files (x86)\\Windows Kits\\10\\bin' _DEFAULT_WIN_SDK_VERSION = '10.0.22621.0' +_BACKUP_WIN_SDK_VERSION = '10.0.22000.0' _SOURCE_SPLASH_SCREEN_SUB_PATH = os.path.join('internal', 'cobalt', 'browser', 'splash_screen') # The splash screen file referenced in starboard/xb1/shared/configuration.cc @@ -89,7 +90,13 @@ def _GetSourceSplashScreenDir(): def GetWinToolsPath() -> str: windows_sdk_bin_dir = _SelectBestPath('WindowsSdkBinPath', _DEFAULT_SDK_BIN_DIR) - return os.path.join(windows_sdk_bin_dir, _DEFAULT_WIN_SDK_VERSION, 'x64') + + # This check can be removed once it's confirmed our builders are using the new + # version of the win sdk. + path = os.path.join(windows_sdk_bin_dir, _DEFAULT_WIN_SDK_VERSION, 'x64') + if os.path.exists(path): + return path + return os.path.join(windows_sdk_bin_dir, _BACKUP_WIN_SDK_VERSION, 'x64') class Package(package.PackageBase): From 24fe739d3f361a33f2fbb77989d163dde0934fe8 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 11:41:01 -0700 Subject: [PATCH 065/140] Cherry pick PR #1643: Add missing cstdint includes (#1653) Refer to the original PR: https://github.com/youtube/cobalt/pull/1643 Co-authored-by: Andrew Savage --- third_party/angle/include/GLSLANG/ShaderVars.h | 1 + third_party/angle/src/common/angleutils.h | 1 + third_party/crashpad/util/misc/reinterpret_bytes.cc | 1 + third_party/v8/src/base/logging.h | 1 + third_party/v8/src/base/macros.h | 1 + third_party/v8/src/inspector/v8-string-conversions.h | 1 + 6 files changed, 6 insertions(+) diff --git a/third_party/angle/include/GLSLANG/ShaderVars.h b/third_party/angle/include/GLSLANG/ShaderVars.h index 52f6ad077556..5d2d14683604 100644 --- a/third_party/angle/include/GLSLANG/ShaderVars.h +++ b/third_party/angle/include/GLSLANG/ShaderVars.h @@ -12,6 +12,7 @@ #include #include +#include #include #include diff --git a/third_party/angle/src/common/angleutils.h b/third_party/angle/src/common/angleutils.h index 3a1391e29b72..c8d36f551861 100644 --- a/third_party/angle/src/common/angleutils.h +++ b/third_party/angle/src/common/angleutils.h @@ -14,6 +14,7 @@ #include #include #include +#include #include #include #include diff --git a/third_party/crashpad/util/misc/reinterpret_bytes.cc b/third_party/crashpad/util/misc/reinterpret_bytes.cc index 65ec33f348ec..3fee722dea39 100644 --- a/third_party/crashpad/util/misc/reinterpret_bytes.cc +++ b/third_party/crashpad/util/misc/reinterpret_bytes.cc @@ -17,6 +17,7 @@ #include #include +#include #include "base/logging.h" diff --git a/third_party/v8/src/base/logging.h b/third_party/v8/src/base/logging.h index fe39f988225e..dbe130581cb1 100644 --- a/third_party/v8/src/base/logging.h +++ b/third_party/v8/src/base/logging.h @@ -5,6 +5,7 @@ #ifndef V8_BASE_LOGGING_H_ #define V8_BASE_LOGGING_H_ +#include #include #include #include diff --git a/third_party/v8/src/base/macros.h b/third_party/v8/src/base/macros.h index 515a9e3cf2ab..a7cd7c00f2ee 100644 --- a/third_party/v8/src/base/macros.h +++ b/third_party/v8/src/base/macros.h @@ -5,6 +5,7 @@ #ifndef V8_BASE_MACROS_H_ #define V8_BASE_MACROS_H_ +#include #include #include diff --git a/third_party/v8/src/inspector/v8-string-conversions.h b/third_party/v8/src/inspector/v8-string-conversions.h index c1d69c18f0a8..eb33c6816a58 100644 --- a/third_party/v8/src/inspector/v8-string-conversions.h +++ b/third_party/v8/src/inspector/v8-string-conversions.h @@ -5,6 +5,7 @@ #ifndef V8_INSPECTOR_V8_STRING_CONVERSIONS_H_ #define V8_INSPECTOR_V8_STRING_CONVERSIONS_H_ +#include #include // Conversion routines between UT8 and UTF16, used by string-16.{h,cc}. You may From 233df5917807ccd91dd4a3cee3a9f81d9e496170 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:26:07 -0700 Subject: [PATCH 066/140] Cherry pick PR #924: Remove unused files (#1024) "Refer to the original PR: https://github.com/youtube/cobalt/pull/924" Co-authored-by: Niranjan Yardi --- components/metrics/BUILD.gn | 5 +++++ components/variations/BUILD.gn | 11 +++++++++++ 2 files changed, 16 insertions(+) diff --git a/components/metrics/BUILD.gn b/components/metrics/BUILD.gn index 53e47b406ff6..e69be69ccaf8 100644 --- a/components/metrics/BUILD.gn +++ b/components/metrics/BUILD.gn @@ -141,6 +141,11 @@ static_library("metrics") { "system_memory_stats_recorder_win.cc", "system_session_analyzer_win.cc", "system_session_analyzer_win.h", + + # Files with unused symbols. ex: DriveMetricsProvider::HasSeekPenalty + # These files give linker errors about undefined symbols during modualr builds. + "drive_metrics_provider.cc", + "drive_metrics_provider.h", ] } diff --git a/components/variations/BUILD.gn b/components/variations/BUILD.gn index dd6fa7bfce30..46eba0c805c6 100644 --- a/components/variations/BUILD.gn +++ b/components/variations/BUILD.gn @@ -87,6 +87,17 @@ static_library("variations") { ] } + if (use_cobalt_customizations) { + # Files with unused symbols. ex: base::FeatureList::RegisterFieldTrialOverride + # These files give linker errors about undefined symbols during modualr builds. + sources -= [ + "variations_seed_processor.cc", + "variations_seed_processor.h", + "variations_seed_simulator.cc", + "variations_seed_simulator.h", + ] + } + if (!use_cobalt_customizations && (is_android || is_ios)) { sources += [ "variations_request_scheduler_mobile.cc", From 9f7d7578ea1c37972424cc4a82593e49f2e0ca39 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:51:30 -0700 Subject: [PATCH 067/140] Cherry pick PR #1613: [Watchdog] Increase max ping count and length. (#1635) Refer to the original PR: https://github.com/youtube/cobalt/pull/1613 Increase max ping count from 20 to 60 and max ping length from 128 to 1024. b/301313509 Co-authored-by: Brian Ting --- cobalt/watchdog/watchdog.cc | 4 ++-- cobalt/watchdog/watchdog_test.cc | 16 ++++------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/cobalt/watchdog/watchdog.cc b/cobalt/watchdog/watchdog.cc index 56e19dd4b1a2..f328ec100b8a 100644 --- a/cobalt/watchdog/watchdog.cc +++ b/cobalt/watchdog/watchdog.cc @@ -43,9 +43,9 @@ const int kWatchdogMaxViolations = 200; // The minimum number of microseconds between writes. const int64_t kWatchdogWriteWaitTime = 300000000; // The maximum number of most recent ping infos. -const int kWatchdogMaxPingInfos = 20; +const int kWatchdogMaxPingInfos = 60; // The maximum length of each ping info. -const int kWatchdogMaxPingInfoLength = 128; +const int kWatchdogMaxPingInfoLength = 1024; // The maximum number of milliseconds old of an unfetched Watchdog violation. const int64_t kWatchdogMaxViolationsAge = 86400000; diff --git a/cobalt/watchdog/watchdog_test.cc b/cobalt/watchdog/watchdog_test.cc index ec3116cf3f95..6668c2a8d9e0 100644 --- a/cobalt/watchdog/watchdog_test.cc +++ b/cobalt/watchdog/watchdog_test.cc @@ -199,11 +199,7 @@ TEST_F(WatchdogTest, PingOnlyAcceptsValidParameters) { base::kApplicationStateStarted, kWatchdogMonitorFrequency)); ASSERT_TRUE(watchdog_->Ping("test-name", "42")); - ASSERT_FALSE( - watchdog_->Ping("test-name", - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - "xxxxxxxxxxxxxxxxxxxxxxxxxxx")); + ASSERT_FALSE(watchdog_->Ping("test-name", std::string(1025, 'x'))); ASSERT_TRUE(watchdog_->Unregister("test-name")); } @@ -213,11 +209,7 @@ TEST_F(WatchdogTest, PingByClientOnlyAcceptsValidParameters) { kWatchdogMonitorFrequency); ASSERT_NE(client, nullptr); ASSERT_TRUE(watchdog_->PingByClient(client, "42")); - ASSERT_FALSE(watchdog_->PingByClient( - client, - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" - "xxxxxxxxxxxxxxxxxxxxxxxxxxx")); + ASSERT_FALSE(watchdog_->PingByClient(client, std::string(1025, 'x'))); ASSERT_TRUE(watchdog_->UnregisterByClient(client)); } @@ -358,7 +350,7 @@ TEST_F(WatchdogTest, PingInfosAreEvictedAfterMax) { ASSERT_TRUE(watchdog_->Register("test-name", "test_desc", base::kApplicationStateStarted, kWatchdogMonitorFrequency)); - for (int i = 0; i < 21; i++) { + for (int i = 0; i < 61; i++) { ASSERT_TRUE(watchdog_->Ping("test-name", std::to_string(i))); } SbThreadSleep(kWatchdogSleepDuration); @@ -369,7 +361,7 @@ TEST_F(WatchdogTest, PingInfosAreEvictedAfterMax) { base::Value* violation_dict = violations_map->FindKey("test-name"); base::Value* violations = violation_dict->FindKey("violations"); base::Value* pingInfos = violations->GetList()[0].FindKey("pingInfos"); - ASSERT_EQ(pingInfos->GetList().size(), 20); + ASSERT_EQ(pingInfos->GetList().size(), 60); ASSERT_EQ(pingInfos->GetList()[0].FindKey("info")->GetString(), "1"); ASSERT_TRUE(watchdog_->Unregister("test-name")); } From 271b6108f775e1606434c1932f90d41dd98e9fa9 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 14:46:54 -0700 Subject: [PATCH 068/140] Cherry pick PR #1267: Remove repeated arguments from clang toolchain (#1667) Refer to the original PR: https://github.com/youtube/cobalt/pull/1267 b/294450490 Co-authored-by: Arjun Menon --- starboard/build/toolchain/BUILD.gn | 3 --- 1 file changed, 3 deletions(-) diff --git a/starboard/build/toolchain/BUILD.gn b/starboard/build/toolchain/BUILD.gn index 21a6a3c48bcb..5f665dba6a1a 100644 --- a/starboard/build/toolchain/BUILD.gn +++ b/starboard/build/toolchain/BUILD.gn @@ -64,8 +64,5 @@ if (is_host_win) { if (defined(native_snarl_linker)) { using_snarl_linker = true } - toolchain_args = { - is_clang = true - } } } From 0c407a5fb78b32302721feb1493fbd1967d4a156 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:14:00 -0700 Subject: [PATCH 069/140] Cherry pick PR #1522: Support Cobalt modular toolchain defintion for NX (#1668) Refer to the original PR: https://github.com/youtube/cobalt/pull/1522 NX compilation requires tail lib dependencies when building shared library targets. b/246855300 Co-authored-by: Arjun Menon --- starboard/build/toolchain/cobalt_toolchains.gni | 1 + 1 file changed, 1 insertion(+) diff --git a/starboard/build/toolchain/cobalt_toolchains.gni b/starboard/build/toolchain/cobalt_toolchains.gni index f88cab4c2d5f..4277164968b8 100644 --- a/starboard/build/toolchain/cobalt_toolchains.gni +++ b/starboard/build/toolchain/cobalt_toolchains.gni @@ -21,6 +21,7 @@ template("cobalt_clang_toolchain") { [ "native_linker_path", "executable_extension", + "tail_lib_dependencies", "shlib_extension", ]) assert(defined(native_linker_path), From 3ba8d414319f4fbd3244ce17025798ae9e3456b2 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:48:48 -0700 Subject: [PATCH 070/140] Cherry pick PR #1652: Enable platform-specific sources to compile loader (#1671) Refer to the original PR: https://github.com/youtube/cobalt/pull/1652 The loader app (for modular toolchain builds) currently only relies on starboard_loader.cc as the source. For NX, we require additional sources, and those can be provided by setting the value of the newly added variable extra_platform_loader_sources in the configuration.gni file for that platform. b/246855300 Co-authored-by: Arjun Menon --- starboard/build/config/BUILDCONFIG.gn | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn index 022b8a156fef..b0b35e0754b9 100644 --- a/starboard/build/config/BUILDCONFIG.gn +++ b/starboard/build/config/BUILDCONFIG.gn @@ -421,6 +421,10 @@ template("shared_library") { forward_variables_from(invoker, [ "testonly" ]) sources = [ "//$starboard_path/starboard_loader.cc" ] + if (defined(extra_platform_loader_sources)) { + sources += extra_platform_loader_sources + } + if (use_asan) { sources += [ "//$starboard_path/sanitizer_options.cc" ] } From 2944f2faee1d35ba88f6d9bc501cea19de23185f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 15:49:44 -0700 Subject: [PATCH 071/140] Cherry pick PR #1634: Increase the timeout for XB1 Net Args service (#1670) Refer to the original PR: https://github.com/youtube/cobalt/pull/1634 b/299672207 Co-authored-by: Arjun Menon --- starboard/shared/uwp/application_uwp.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/starboard/shared/uwp/application_uwp.cc b/starboard/shared/uwp/application_uwp.cc index c1192f542e52..b0f18dced91e 100644 --- a/starboard/shared/uwp/application_uwp.cc +++ b/starboard/shared/uwp/application_uwp.cc @@ -660,7 +660,8 @@ ref class App sealed : public IFrameworkView { TryAddCommandArgsFromStarboardFile(&args_); CommandLine cmd_line(args_); if (cmd_line.HasSwitch(kNetArgsCommandSwitchWait)) { - SbTime timeout = kSbTimeSecond * 2; + // Wait for net args is flaky and needs extended wait time on Xbox. + SbTime timeout = kSbTimeSecond * 30; std::string val = cmd_line.GetSwitchValue(kNetArgsCommandSwitchWait); if (!val.empty()) { timeout = atoi(val.c_str()); From f59326fb8cb0e53ba7a3f1544a8f7738d2a2d6a9 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 28 Sep 2023 17:58:31 -0700 Subject: [PATCH 072/140] Cherry pick PR #1536: Add check for starboard python module (#1674) Refer to the original PR: https://github.com/youtube/cobalt/pull/1536 b/299098707 --------- Co-authored-by: Oscar Vestlie Co-authored-by: Arjun Menon --- starboard/build/config/BUILDCONFIG.gn | 4 ++++ starboard/build/is_on_path.py | 32 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 starboard/build/is_on_path.py diff --git a/starboard/build/config/BUILDCONFIG.gn b/starboard/build/config/BUILDCONFIG.gn index b0b35e0754b9..315e4024ee89 100644 --- a/starboard/build/config/BUILDCONFIG.gn +++ b/starboard/build/config/BUILDCONFIG.gn @@ -37,6 +37,10 @@ declare_args() { build_with_separate_cobalt_toolchain = false } +_is_on_pythonpath = exec_script("//starboard/build/is_on_path.py", [], "json") +assert(!is_starboard || _is_on_pythonpath, + "The current repo is not first on the PYTHONPATH.") + assert(!(is_starboard && is_native_target_build), "Targets should be built for Starboard or natively, but not both") diff --git a/starboard/build/is_on_path.py b/starboard/build/is_on_path.py new file mode 100644 index 000000000000..c9c149bd78b2 --- /dev/null +++ b/starboard/build/is_on_path.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +# Copyright 2023 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. +"""Script for checking if the current repo on the path.""" + +import os + + +def main(): + try: + # Try to import this file and compare its path to the current file. + import starboard.build.is_on_path # pylint: disable=import-outside-toplevel + this_file = os.path.realpath(__file__) + imported_file = os.path.realpath(starboard.build.is_on_path.__file__) + print(str(this_file == imported_file).lower()) + except ImportError: + print('false') + + +if __name__ == '__main__': + main() From b01f72f51f3cf5cf6011ff71e8097a2fa6b8f6a7 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:28:57 -0700 Subject: [PATCH 073/140] Cherry pick PR #1686: Add missing header to lz4 tool (#1687) Refer to the original PR: https://github.com/youtube/cobalt/pull/1686 b/302751747 Co-authored-by: Andrew Savage --- tools/lz4_compress/lz4_compress.cc | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/lz4_compress/lz4_compress.cc b/tools/lz4_compress/lz4_compress.cc index 26421dcf6f50..08a79f532e23 100644 --- a/tools/lz4_compress/lz4_compress.cc +++ b/tools/lz4_compress/lz4_compress.cc @@ -22,6 +22,7 @@ #include +#include #include #include From cb8501c0e9f6bdabb580f4b8656a193a66e0c024 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 2 Oct 2023 14:37:49 -0700 Subject: [PATCH 074/140] Cherry pick PR #1669: [UWP] Remove potentially problematic logging line (#1685) Refer to the original PR: https://github.com/youtube/cobalt/pull/1669 StatsTracker attempts to log in a specific case while a file operation is occurring. For UWP logging is a file operation, so the log file was already being held by the same thread and the mutex couldn't be acquired. b/301650853 Change-Id: I6a315b1223ebcb0f2a54c97f590d42e798a65eb8 Co-authored-by: Tyler Holcombe --- starboard/common/metrics/stats_tracker.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/starboard/common/metrics/stats_tracker.h b/starboard/common/metrics/stats_tracker.h index 940617572c90..7a217d992b6f 100644 --- a/starboard/common/metrics/stats_tracker.h +++ b/starboard/common/metrics/stats_tracker.h @@ -52,8 +52,6 @@ class StatsTrackerContainer { StatsTracker& stats_tracker() { if (!stats_tracker_) { - SB_DLOG_IF(ERROR, !undefined_logged_) - << "[once] StatsTracker is not defined."; undefined_logged_ = true; return undefined_stats_tracker_; } From 1c2aab943ff404e9e87369aad0482696f6a298e5 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:35:06 -0700 Subject: [PATCH 075/140] Cherry pick PR #1250: [XB1] Change GetSignature to return status (#1694) Refer to the original PR: https://github.com/youtube/cobalt/pull/1250 Update GetSignature to return true if the signature was successfully populated and false otherwise. Full change at go/cobalt-cl/259900 b/294450861 Change-Id: I12668d2ba132d7f3b78899ac90113adb1b9046da Co-authored-by: Tyler Holcombe --- starboard/xb1/shared/internal_shims.h | 2 +- starboard/xb1/shared/internal_stubs.cc | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/starboard/xb1/shared/internal_shims.h b/starboard/xb1/shared/internal_shims.h index 9cd715340ee8..877ed8b57fbb 100644 --- a/starboard/xb1/shared/internal_shims.h +++ b/starboard/xb1/shared/internal_shims.h @@ -31,7 +31,7 @@ void Release(); Platform::String ^ GetCertScope(); -void GetSignature(Windows::Storage::Streams::IBuffer ^ message_buffer, +bool GetSignature(Windows::Storage::Streams::IBuffer ^ message_buffer, Windows::Storage::Streams::IBuffer ^ *signature); } // namespace shared diff --git a/starboard/xb1/shared/internal_stubs.cc b/starboard/xb1/shared/internal_stubs.cc index a8f6ba561b5a..b243b4bd3790 100644 --- a/starboard/xb1/shared/internal_stubs.cc +++ b/starboard/xb1/shared/internal_stubs.cc @@ -33,8 +33,10 @@ void Release() {} Platform::String ^ GetCertScope() { return ""; } -void GetSignature(Windows::Storage::Streams::IBuffer ^ message_buffer, - Windows::Storage::Streams::IBuffer ^ *signature) {} +bool GetSignature(Windows::Storage::Streams::IBuffer ^ message_buffer, + Windows::Storage::Streams::IBuffer ^ *signature) { + return false; +} // clang-format on } // namespace shared From 97f5520e0f93bbb9a52730c690671f2283daa688 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 2 Oct 2023 16:48:20 -0700 Subject: [PATCH 076/140] Cherry pick PR #1651: Cleanup video_stream_ & audio_stream_ after demuxer_ stops (#1678) Refer to the original PR: https://github.com/youtube/cobalt/pull/1651 Native crash due to fault address of video_stream_, cleanup video_stream_ & audio_stream_ after demuxer_ stops. b/301284907 Co-authored-by: Bo-Rong Chen --- cobalt/media/base/sbplayer_pipeline.cc | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc index 8280e53fc685..55bf81865051 100644 --- a/cobalt/media/base/sbplayer_pipeline.cc +++ b/cobalt/media/base/sbplayer_pipeline.cc @@ -339,6 +339,8 @@ void SbPlayerPipeline::Stop(const base::Closure& stop_cb) { if (demuxer_) { stop_cb_ = stop_cb; demuxer_->Stop(); + video_stream_ = nullptr; + audio_stream_ = nullptr; OnDemuxerStopped(); } else { stop_cb.Run(); @@ -1371,6 +1373,8 @@ void SbPlayerPipeline::ResumeTask(PipelineWindow window, std::string SbPlayerPipeline::AppendStatisticsString( const std::string& message) const { + DCHECK(task_runner_->BelongsToCurrentThread()); + if (nullptr == video_stream_) { return message + ", playback statistics: n/a."; } else { From 6359e4e2c53aaf92aa4fd5366a4f627d282e8321 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 3 Oct 2023 14:40:16 -0700 Subject: [PATCH 077/140] Cherry pick PR #1703: [android] Fix startup crash on some devices (#1707) Refer to the original PR: https://github.com/youtube/cobalt/pull/1703 The statement `MediaCodec.createByCodecName(name).getName()` used to get the canonical name to check whether the decoder supports copy free operations on LinearBlock triggers a crash on some devices. Remove it to unblock launch. b/302689405 Co-authored-by: xiaomings --- .../media/MediaCodecCapabilitiesLogger.java | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java index 079f89784f6a..c30275d1c669 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java @@ -16,12 +16,10 @@ import static dev.cobalt.media.Log.TAG; -import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.VideoCapabilities; import android.media.MediaCodecList; -import android.os.Build; import dev.cobalt.util.Log; import java.util.ArrayList; import java.util.Arrays; @@ -172,31 +170,6 @@ private static void ensurefeatureMapInitialized() { return codecCapabilities.isFeatureSupported( MediaCodecInfo.CodecCapabilities.FEATURE_PartialFrame); }); - featureMap.put( - "LinearBlockCopyFree", - (name, codecCapabilities) -> { - if (Build.VERSION.SDK_INT < 30) { - // MediaCodec.LinearBlock is introduced in api level 30. - return false; - } - VideoCapabilities videoCapabilities = codecCapabilities.getVideoCapabilities(); - if (videoCapabilities == null) { - return false; - } - try { - String canonicalName = MediaCodec.createByCodecName(name).getName(); - String[] codecNames = new String[] {canonicalName}; - return MediaCodec.LinearBlock.isCodecCopyFreeCompatible(codecNames); - } catch (Exception e) { - Log.e( - TAG, - "Failed to create MediaCodec or call isCodecCopyFreeCompatible() on codec name" - + " \"%s\" with error %s", - name, - e); - return false; - } - }); featureMap.put( "SecurePlayback", (name, codecCapabilities) -> { From 076526426120e7b4bef7d33268aa84ef094bb583 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 3 Oct 2023 17:33:04 -0700 Subject: [PATCH 078/140] Cherry pick PR #1644: Create ScreenShotWriter thread only when necessary (#1712) Refer to the original PR: https://github.com/youtube/cobalt/pull/1644 This delays creating the ScreenShotWriter until the first time a ScreenShot is requested. b/302358421 Co-authored-by: Jelle Foks --- cobalt/browser/browser_module.cc | 31 +++++++++++++++++++++--- cobalt/browser/browser_module.h | 11 +++++++++ cobalt/webdriver/testdata/simple_test.py | 2 +- 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/cobalt/browser/browser_module.cc b/cobalt/browser/browser_module.cc index abe8c4298521..f44b2b2672fc 100644 --- a/cobalt/browser/browser_module.cc +++ b/cobalt/browser/browser_module.cc @@ -691,8 +691,8 @@ void BrowserModule::NavigateCreateWebModule( } options.provide_screenshot_function = - base::Bind(&ScreenShotWriter::RequestScreenshotToMemoryUnencoded, - base::Unretained(screen_shot_writer_.get())); + base::Bind(&BrowserModule::RequestScreenshotToMemoryUnencoded, + base::Unretained(this)); #if defined(ENABLE_DEBUGGER) if (base::CommandLine::ForCurrentProcess()->HasSwitch( @@ -806,6 +806,14 @@ bool BrowserModule::WaitForLoad(const base::TimeDelta& timeout) { return web_module_loaded_.TimedWait(timeout); } +void BrowserModule::EnsureScreenShotWriter() { + if (!screen_shot_writer_ && renderer_module_) { + screen_shot_writer_.reset( + new ScreenShotWriter(renderer_module_->pipeline())); + } +} + +#if defined(ENABLE_WEBDRIVER) || defined(ENABLE_DEBUGGER) void BrowserModule::RequestScreenshotToFile( const base::FilePath& path, loader::image::EncodedStaticImage::ImageFormat image_format, @@ -813,7 +821,9 @@ void BrowserModule::RequestScreenshotToFile( const base::Closure& done_callback) { TRACE_EVENT0("cobalt::browser", "BrowserModule::RequestScreenshotToFile()"); DCHECK_EQ(base::MessageLoop::current(), self_message_loop_); + EnsureScreenShotWriter(); DCHECK(screen_shot_writer_); + if (!screen_shot_writer_) return; scoped_refptr render_tree; web_module_->DoSynchronousLayoutAndGetRenderTree(&render_tree); @@ -831,7 +841,9 @@ void BrowserModule::RequestScreenshotToMemory( const base::Optional& clip_rect, const ScreenShotWriter::ImageEncodeCompleteCallback& screenshot_ready) { TRACE_EVENT0("cobalt::browser", "BrowserModule::RequestScreenshotToMemory()"); + EnsureScreenShotWriter(); DCHECK(screen_shot_writer_); + if (!screen_shot_writer_) return; // Note: This does not have to be called from self_message_loop_. scoped_refptr render_tree; @@ -844,6 +856,19 @@ void BrowserModule::RequestScreenshotToMemory( screen_shot_writer_->RequestScreenshotToMemory(image_format, render_tree, clip_rect, screenshot_ready); } +#endif // defined(ENABLE_WEBDRIVER) || defined(ENABLE_DEBUGGER) + +// Request a screenshot to memory without compressing the image. +void BrowserModule::RequestScreenshotToMemoryUnencoded( + const scoped_refptr& render_tree_root, + const base::Optional& clip_rect, + const renderer::Pipeline::RasterizationCompleteCallback& callback) { + EnsureScreenShotWriter(); + DCHECK(screen_shot_writer_); + if (!screen_shot_writer_) return; + screen_shot_writer_->RequestScreenshotToMemoryUnencoded(render_tree_root, + clip_rect, callback); +} void BrowserModule::ProcessRenderTreeSubmissionQueue() { TRACE_EVENT0("cobalt::browser", @@ -1794,7 +1819,7 @@ void BrowserModule::InstantiateRendererModule() { system_window_.get(), RendererModuleWithCameraOptions(options_.renderer_module_options, input_device_manager_->camera_3d()))); - screen_shot_writer_.reset(new ScreenShotWriter(renderer_module_->pipeline())); + screen_shot_writer_.reset(); } void BrowserModule::DestroyRendererModule() { diff --git a/cobalt/browser/browser_module.h b/cobalt/browser/browser_module.h index a2d24a2156dc..abf05fd8f5e0 100644 --- a/cobalt/browser/browser_module.h +++ b/cobalt/browser/browser_module.h @@ -152,6 +152,10 @@ class BrowserModule { void AddURLHandler(const URLHandler::URLHandlerCallback& callback); void RemoveURLHandler(const URLHandler::URLHandlerCallback& callback); + // Start the ScreenShotWriter if it's not already running. + void EnsureScreenShotWriter(); + +#if defined(ENABLE_WEBDRIVER) || defined(ENABLE_DEBUGGER) // Request a screenshot to be written to the specified path. Callback will // be fired after the screenshot has been written to disk. void RequestScreenshotToFile( @@ -165,6 +169,13 @@ class BrowserModule { loader::image::EncodedStaticImage::ImageFormat image_format, const base::Optional& clip_rect, const ScreenShotWriter::ImageEncodeCompleteCallback& screenshot_ready); +#endif // defined(ENABLE_WEBDRIVER) || defined(ENABLE_DEBUGGER) + + // Request a screenshot to memory without compressing the image. + void RequestScreenshotToMemoryUnencoded( + const scoped_refptr& render_tree_root, + const base::Optional& clip_rect, + const renderer::Pipeline::RasterizationCompleteCallback& callback); #if defined(ENABLE_WEBDRIVER) std::unique_ptr CreateSessionDriver( diff --git a/cobalt/webdriver/testdata/simple_test.py b/cobalt/webdriver/testdata/simple_test.py index 5b5a9a100637..168436bf76e7 100755 --- a/cobalt/webdriver/testdata/simple_test.py +++ b/cobalt/webdriver/testdata/simple_test.py @@ -176,7 +176,7 @@ def GetElementScreenShot(session_id, element_id, filename): """ request = ElementRequest(session_id, element_id, GET, 'screenshot') if request: - with open(filename, 'w', encoding='utf-8') as f: + with open(filename, 'wb') as f: f.write(binascii.a2b_base64(request['value'])) f.close() From 0bfb5f98e17a85eba4d15b9f59d18f9be89dd63a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 4 Oct 2023 09:35:53 -0700 Subject: [PATCH 079/140] Cherry pick PR #1557: [android] Avoid using uninitialized drm system (#1646) Refer to the original PR: https://github.com/youtube/cobalt/pull/1557 b/299150189 Co-authored-by: Jason --- starboard/android/shared/media_decoder.cc | 26 ++++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/starboard/android/shared/media_decoder.cc b/starboard/android/shared/media_decoder.cc index 74bb4a738bdc..cf93733b38b1 100644 --- a/starboard/android/shared/media_decoder.cc +++ b/starboard/android/shared/media_decoder.cc @@ -469,24 +469,20 @@ bool MediaDecoder::ProcessOneInputBuffer( } jint status; - if (event.type == Event::kWriteCodecConfig) { - if (!drm_system_ || (drm_system_ && drm_system_->IsReady())) { - status = media_codec_bridge_->QueueInputBuffer(dequeue_input_result.index, - kNoOffset, size, kNoPts, - BUFFER_FLAG_CODEC_CONFIG); - } else { - status = MEDIA_CODEC_NO_KEY; - } + if (drm_system_ && !drm_system_->IsReady()) { + // Drm system initialization is asynchronous. If there's a drm system, we + // should wait until it's initialized to avoid errors. + status = MEDIA_CODEC_NO_KEY; + } else if (event.type == Event::kWriteCodecConfig) { + status = media_codec_bridge_->QueueInputBuffer(dequeue_input_result.index, + kNoOffset, size, kNoPts, + BUFFER_FLAG_CODEC_CONFIG); } else if (event.type == Event::kWriteInputBuffer) { jlong pts_us = input_buffer->timestamp(); if (drm_system_ && input_buffer->drm_info()) { - if (drm_system_->IsReady()) { - status = media_codec_bridge_->QueueSecureInputBuffer( - dequeue_input_result.index, kNoOffset, *input_buffer->drm_info(), - pts_us); - } else { - status = MEDIA_CODEC_NO_KEY; - } + status = media_codec_bridge_->QueueSecureInputBuffer( + dequeue_input_result.index, kNoOffset, *input_buffer->drm_info(), + pts_us); } else { status = media_codec_bridge_->QueueInputBuffer( dequeue_input_result.index, kNoOffset, size, pts_us, kNoBufferFlags); From d043ce4950e8ee38c22e4899be98206106bd0527 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 4 Oct 2023 13:43:24 -0700 Subject: [PATCH 080/140] Cherry pick PR #1587: [Android] Improvements around application exit. (#1720) Refer to the original PR: https://github.com/youtube/cobalt/pull/1587 This adds a flag to break out of the loop waiting for window creation/destruction when the application exits. This also adds two more checks for starboardStopped to ensure that search and deep link don't dereference the application object after destruction. b/225209442 b/301158281 b/301161359 Co-authored-by: Jelle Foks --- .../src/main/java/dev/cobalt/coat/StarboardBridge.java | 9 +++++++-- starboard/android/shared/application_android.cc | 3 ++- starboard/android/shared/application_android.h | 4 ++++ 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java index d63645b7fe49..b9134ec6a16b 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java @@ -305,7 +305,10 @@ public void requestSuspend() { } public boolean onSearchRequested() { - return nativeOnSearchRequested(); + if (!starboardStopped) { + return nativeOnSearchRequested(); + } + return false; } private native boolean nativeOnSearchRequested(); @@ -349,7 +352,9 @@ protected String getStartDeepLink() { /** Sends an event to the web app to navigate to the given URL */ public void handleDeepLink(String url) { - nativeHandleDeepLink(url); + if (!starboardStopped) { + nativeHandleDeepLink(url); + } } private native void nativeHandleDeepLink(String url); diff --git a/starboard/android/shared/application_android.cc b/starboard/android/shared/application_android.cc index 33b26ad01c49..61b7a8471db6 100644 --- a/starboard/android/shared/application_android.cc +++ b/starboard/android/shared/application_android.cc @@ -161,6 +161,7 @@ ApplicationAndroid::~ApplicationAndroid() { { // Signal for any potentially waiting window creation or destroy commands. ScopedLock lock(android_command_mutex_); + application_destroying_.store(true); android_command_condition_.Signal(); } } @@ -412,7 +413,7 @@ void ApplicationAndroid::SendAndroidCommand(AndroidCommand::CommandType type, switch (type) { case AndroidCommand::kNativeWindowCreated: case AndroidCommand::kNativeWindowDestroyed: - while (native_window_ != data) { + while ((native_window_ != data) && !application_destroying_.load()) { android_command_condition_.Wait(); } break; diff --git a/starboard/android/shared/application_android.h b/starboard/android/shared/application_android.h index e8917c13c4cf..bca3b1f47da2 100644 --- a/starboard/android/shared/application_android.h +++ b/starboard/android/shared/application_android.h @@ -25,6 +25,7 @@ #include "starboard/android/shared/input_events_generator.h" #include "starboard/android/shared/jni_env_ext.h" #include "starboard/atomic.h" +#include "starboard/common/atomic.h" #include "starboard/common/condition_variable.h" #include "starboard/common/mutex.h" #include "starboard/common/scoped_ptr.h" @@ -154,6 +155,9 @@ class ApplicationAndroid // already requested it be stopped. SbAtomic32 android_stop_count_ = 0; + // Set to true in the destructor to ensure other threads stop waiting. + atomic_bool application_destroying_; + // The last Activity lifecycle state command received. AndroidCommand::CommandType activity_state_; From 98b0fc7a6794fd0dd510939c1cb692d87b361972 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 4 Oct 2023 15:12:41 -0700 Subject: [PATCH 081/140] Cherry pick PR #1690: Fix discarded duration overflow (#1725) Refer to the original PR: https://github.com/youtube/cobalt/pull/1690 b/302020010 Co-authored-by: Jason --- .../player/decoded_audio_internal.cc | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/starboard/shared/starboard/player/decoded_audio_internal.cc b/starboard/shared/starboard/player/decoded_audio_internal.cc index 2834bc225e53..0d90fe94f798 100644 --- a/starboard/shared/starboard/player/decoded_audio_internal.cc +++ b/starboard/shared/starboard/player/decoded_audio_internal.cc @@ -142,17 +142,26 @@ void DecodedAudio::AdjustForDiscardedDurations( SB_DCHECK(discarded_duration_from_back >= 0); SB_DCHECK(storage_type() == kSbMediaAudioFrameStorageTypeInterleaved); - const auto bytes_per_frame = GetBytesPerSample(sample_type()) * channels_; - auto discarded_frames_from_front = - AudioDurationToFrames(discarded_duration_from_front, sample_rate); + if (discarded_duration_from_front == 0 && discarded_duration_from_back == 0) { + return; + } - discarded_frames_from_front = std::min(discarded_frames_from_front, frames()); + const auto bytes_per_frame = GetBytesPerSample(sample_type()) * channels_; + int current_frames = frames(); + int discarded_frames_from_front = + (discarded_duration_from_front >= + AudioFramesToDuration(current_frames, sample_rate)) + ? current_frames + : AudioDurationToFrames(discarded_duration_from_front, sample_rate); offset_in_bytes_ += bytes_per_frame * discarded_frames_from_front; size_in_bytes_ -= bytes_per_frame * discarded_frames_from_front; - auto discarded_frames_from_back = - AudioDurationToFrames(discarded_duration_from_back, sample_rate); - discarded_frames_from_back = std::min(discarded_frames_from_back, frames()); + current_frames = frames(); + int discarded_frames_from_back = + (discarded_duration_from_back >= + AudioFramesToDuration(current_frames, sample_rate)) + ? current_frames + : AudioDurationToFrames(discarded_duration_from_back, sample_rate); size_in_bytes_ -= bytes_per_frame * discarded_frames_from_back; } From 073955ab6caa6cd91d4afb4fdc69212e86688d42 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:13:18 -0700 Subject: [PATCH 082/140] Cherry pick PR #1721: Update Android target SDK to 34 (#1726) Refer to the original PR: https://github.com/youtube/cobalt/pull/1721 b/303470768 Co-authored-by: Garo Bournoutian --- base/android/jni_generator/AndroidManifest.xml | 2 +- net/android/unittest_support/AndroidManifest.xml | 2 +- net/test/android/javatests/AndroidManifest.xml | 2 +- starboard/android/apk/app/build.gradle | 2 +- testing/android/AndroidManifest.xml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/base/android/jni_generator/AndroidManifest.xml b/base/android/jni_generator/AndroidManifest.xml index 1555a81aae80..aa0b7c55bf9b 100644 --- a/base/android/jni_generator/AndroidManifest.xml +++ b/base/android/jni_generator/AndroidManifest.xml @@ -7,7 +7,7 @@ - + diff --git a/net/android/unittest_support/AndroidManifest.xml b/net/android/unittest_support/AndroidManifest.xml index d4f4261d5788..63a5fea3ed2b 100644 --- a/net/android/unittest_support/AndroidManifest.xml +++ b/net/android/unittest_support/AndroidManifest.xml @@ -10,7 +10,7 @@ found in the LICENSE file. android:versionCode="1" android:versionName="1.0"> - + diff --git a/net/test/android/javatests/AndroidManifest.xml b/net/test/android/javatests/AndroidManifest.xml index af051a4fa857..9bccb8e40441 100644 --- a/net/test/android/javatests/AndroidManifest.xml +++ b/net/test/android/javatests/AndroidManifest.xml @@ -8,7 +8,7 @@ xmlns:tools="http://schemas.android.com/tools" package="org.chromium.net.test.support"> - + diff --git a/starboard/android/apk/app/build.gradle b/starboard/android/apk/app/build.gradle index b64b5d3b9ab2..a50b50bb3b3f 100644 --- a/starboard/android/apk/app/build.gradle +++ b/starboard/android/apk/app/build.gradle @@ -75,7 +75,7 @@ android { defaultConfig { applicationId "dev.cobalt.coat" minSdkVersion 24 - targetSdkVersion 31 + targetSdkVersion 34 versionCode 1 versionName "${buildId}" manifestPlaceholders = [ diff --git a/testing/android/AndroidManifest.xml b/testing/android/AndroidManifest.xml index 283ebb97c016..6faeeb4be8a5 100644 --- a/testing/android/AndroidManifest.xml +++ b/testing/android/AndroidManifest.xml @@ -10,7 +10,7 @@ found in the LICENSE file. android:versionCode="1" android:versionName="1.0"> - + Date: Wed, 4 Oct 2023 16:40:13 -0700 Subject: [PATCH 083/140] Cherry pick PR #1619: [XB1] Fix missing data for YouTubeTV (#1624) Refer to the original PR: https://github.com/youtube/cobalt/pull/1619 b/299524777 Change-Id: Iec9da5b0417b15385ec338943826dce05045bbad Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/packager.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/starboard/xb1/tools/packager.py b/starboard/xb1/tools/packager.py index 5d769a50b27b..a8b4e5ef0f06 100644 --- a/starboard/xb1/tools/packager.py +++ b/starboard/xb1/tools/packager.py @@ -150,6 +150,15 @@ def _CopySplashScreen(self): return shutil.copy(src_splash_screen_file, splash_screen_dir) + def _CopyAppxData(self): + appx_data_output_dir = os.path.join(self.appx_folder_location, 'content', + 'data') + source_dir = os.path.join(self.source_dir, 'appx', 'content', 'data') + if not os.path.exists(source_dir): + logging.error('Failed to find source content in: %s', source_dir) + return + shutil.copytree(source_dir, appx_data_output_dir, dirs_exist_ok=True) + @classmethod def SupportedPlatforms(cls): if platform.system() == 'Windows': @@ -171,6 +180,11 @@ def __init__(self, publisher, product, **kwargs): if self.publisher: self._UpdateAppxManifestPublisher(publisher) + # For YouTubeTV and MainAppBeta move the appx data content to the correct + # appx directory. + if (self.product in ['youtubetv', 'mainappbeta']): + self._CopyAppxData() + # Remove any previous splash screen from content. self._CleanSplashScreenDir() # Copy the correct splash screen into content. From 64285b4facc2491cfa54316628a07cb663fd4bb2 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 4 Oct 2023 16:40:47 -0700 Subject: [PATCH 084/140] Cherry pick PR #1647: [XB1] Refactor test retry loop to rerun net_args (#1649) Refer to the original PR: https://github.com/youtube/cobalt/pull/1647 b/299672207 Change-Id: Ic90a8805fa9bc6ec7a99ffe55c318f44b82f85cb Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/xb1_launcher.py | 41 +++++++++++++++++++++----- starboard/xb1/tools/xb1_network_api.py | 39 ++++++++++-------------- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/starboard/xb1/tools/xb1_launcher.py b/starboard/xb1/tools/xb1_launcher.py index ab54abd45660..cf252793fa1c 100644 --- a/starboard/xb1/tools/xb1_launcher.py +++ b/starboard/xb1/tools/xb1_launcher.py @@ -95,6 +95,14 @@ _XB1_NET_LOG_PORT = 49353 _XB1_NET_ARG_PORT = 49355 +# Number of times a test will try or retry. +_TEST_MAX_TRIES = 4 +# Seconds to wait between retries (scales with backoff factor). +_TEST_RETRY_WAIT = 8 +# Amount to multiply retry time with each failed attempt (i.e. 2 doubles the +# amount of time to wait between retries). +_TEST_RETRY_BACKOFF_FACTOR = 2 + _PROCESS_TIMEOUT = 60 * 5.0 _PROCESS_KILL_TIMEOUT_SECONDS = 5.0 @@ -448,6 +456,31 @@ def CheckPackageIsDeployed(self): return False return package_list[package_index:].split('\n')[0].strip() + def RunTest(self, appx_name: str): + self.net_args_thread = None + attempt_num = 0 + retry_wait_s = _TEST_RETRY_WAIT + while attempt_num < _TEST_MAX_TRIES: + if not self.net_args_thread or not self.net_args_thread.is_alive(): + # This thread must start before the app executes or else it is possible + # the app will hang at _network_api.ExecuteBinary() + self.net_args_thread = net_args.NetArgsThread(self.device_id, + _XB1_NET_ARG_PORT, + self._target_args) + if self._network_api.ExecuteBinary(_DEFAULT_PACKAGE_NAME, appx_name): + break + + if not self.net_args_thread.ArgsSent(): + self._LogLn( + 'Net Args were not sent to the test! This will likely cause ' + 'the test to fail!') + attempt_num += 1 + self._LogLn(f'Retry attempt {attempt_num}.') + time.sleep(retry_wait_s) + retry_wait_s *= _TEST_RETRY_BACKOFF_FACTOR + if hasattr(self, 'net_args_thread'): + self.net_args_thread.join() + def Run(self): # Only upload and install Appx on the first run. if FirstRun(): @@ -474,12 +507,6 @@ def Run(self): try: self.Kill() # Kill existing running app. - - # These threads must start before the app executes or else it is possible - # the app will hang at _network_api.ExecuteBinary() - self.net_args_thread = net_args.NetArgsThread(self.device_id, - _XB1_NET_ARG_PORT, - self._target_args) # While binary is running, extract the net log and stream it to # the output. self.net_log_thread = net_log.NetLogThread(self.device_id, @@ -487,7 +514,7 @@ def Run(self): appx_name = ToAppxFriendlyName(self.target_name) - self._network_api.ExecuteBinary(_DEFAULT_PACKAGE_NAME, appx_name) + self.RunTest(appx_name) while self._network_api.IsBinaryRunning(self.target_name): self._Log(self.net_log_thread.GetLog()) diff --git a/starboard/xb1/tools/xb1_network_api.py b/starboard/xb1/tools/xb1_network_api.py index 093218e662f7..2d8834e9e363 100644 --- a/starboard/xb1/tools/xb1_network_api.py +++ b/starboard/xb1/tools/xb1_network_api.py @@ -421,7 +421,8 @@ def FetchPackageFile(self, return None return None - def ExecuteBinary(self, partial_package_name, app_alias_name): + def ExecuteBinary(self, partial_package_name: str, + app_alias_name: str) -> bool: default_relative_name = self._GetDefaultRelativeId(partial_package_name) if not default_relative_name or not '!' in default_relative_name: raise IOError('Could not resolve package name "' + partial_package_name + @@ -432,33 +433,25 @@ def ExecuteBinary(self, partial_package_name, app_alias_name): appid_64 = base64.b64encode(package_relative_id.encode('UTF-8')) package_64 = base64.b64encode(default_relative_name.encode('UTF-8')) - retry_count = 4 - # Time to wait between tries. - retry_wait_s = 8 try: - while retry_count > 0: - self.LogLn('Executing: ' + package_relative_id) - response = self._DoJsonRequest( - 'POST', - _TASKMANAGER_ENDPOINT, - params={ - 'appid': appid_64, - 'package': package_64 - }, - raise_on_failure=False) - if not response or response == requests.codes.OK: - self.LogLn('Execution successful') - break - self.LogLn('Execution not successful: ' + str(response)) - self.LogLn('Retrying with ' + str(retry_count) + ' attempts remaining.') - time.sleep(retry_wait_s) - retry_count -= 1 - # Double the wait time until the next attempt. - retry_wait_s *= 2 + self.LogLn('Executing: ' + package_relative_id) + response = self._DoJsonRequest( + 'POST', + _TASKMANAGER_ENDPOINT, + params={ + 'appid': appid_64, + 'package': package_64 + }, + raise_on_failure=False) + if not response or response == requests.codes.OK: + self.LogLn('Execution successful') + return True + self.LogLn('Execution not successful: ' + str(response)) except Exception as err: err_msg = '\n Failed to run:\n ' + package_relative_id + \ '\n because of:\n' + str(err) raise IOError(err_msg) from err + return False # Given a package name, return all files + directories. # Throws IOError if the app is locked. From 8dba81545e7e2e11f4e1d02ef9a1ef31f85f3f7f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 5 Oct 2023 10:07:04 -0700 Subject: [PATCH 085/140] Cherry pick PR #1711: [android] Signal application readyness precisely. (#1719) Refer to the original PR: https://github.com/youtube/cobalt/pull/1711 This signals precisely to the StarboardBridge when the ApplicationAndroid is ready, by calling from the constructor and destructor, and setting or clearing a new starboardApplicationReady flag, used to gate all calls to native methods that can use ApplicationAndroid, to ensure such calls are not made before the constructor finishes or after the destructor starts. The 'applicationStopped' flag is now only used to exit in onActivityDestroy. b/301158281 Co-authored-by: Jelle Foks --- .../java/dev/cobalt/coat/StarboardBridge.java | 32 +++++++++++++++---- .../android/shared/application_android.cc | 6 ++++ starboard/android/shared/jni_env_ext.cc | 2 +- starboard/android/shared/video_window.cc | 14 +++++--- 4 files changed, 41 insertions(+), 13 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java index b9134ec6a16b..dacb2dcc9b7a 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java @@ -96,7 +96,7 @@ public interface HostApplication { private final Holder activityHolder; private final Holder serviceHolder; private final String[] args; - private final String startDeepLink; + private String startDeepLink; private final Runnable stopRequester = new Runnable() { @Override @@ -105,7 +105,8 @@ public void run() { } }; - private volatile boolean starboardStopped = false; + private volatile boolean starboardApplicationStopped = false; + private volatile boolean starboardApplicationReady = false; private final HashMap cobaltServiceFactories = new HashMap<>(); private final HashMap cobaltServices = new HashMap<>(); @@ -166,7 +167,7 @@ protected void onActivityStop(Activity activity) { } protected void onActivityDestroy(Activity activity) { - if (starboardStopped) { + if (starboardApplicationStopped) { // We can't restart the starboard app, so kill the process for a clean start next time. Log.i(TAG, "Activity destroyed after shutdown; killing app."); System.exit(0); @@ -265,7 +266,7 @@ protected void beforeSuspend() { @SuppressWarnings("unused") @UsedByNative protected void afterStopped() { - starboardStopped = true; + starboardApplicationStopped = true; ttsHelper.shutdown(); userAuthorizer.shutdown(); for (CobaltService service : cobaltServices.values()) { @@ -283,10 +284,23 @@ protected void afterStopped() { } } + @SuppressWarnings("unused") + @UsedByNative + protected void starboardApplicationStarted() { + starboardApplicationReady = true; + } + + @SuppressWarnings("unused") + @UsedByNative + protected void starboardApplicationStopping() { + starboardApplicationReady = false; + starboardApplicationStopped = true; + } + @SuppressWarnings("unused") @UsedByNative public void requestStop(int errorLevel) { - if (!starboardStopped) { + if (starboardApplicationReady) { Log.i(TAG, "Request to stop"); nativeStopApp(errorLevel); } @@ -305,7 +319,7 @@ public void requestSuspend() { } public boolean onSearchRequested() { - if (!starboardStopped) { + if (starboardApplicationReady) { return nativeOnSearchRequested(); } return false; @@ -352,8 +366,12 @@ protected String getStartDeepLink() { /** Sends an event to the web app to navigate to the given URL */ public void handleDeepLink(String url) { - if (!starboardStopped) { + if (starboardApplicationReady) { nativeHandleDeepLink(url); + } else { + // If this deep link event is received before the starboard application + // is ready, it replaces the start deep link. + startDeepLink = url; } } diff --git a/starboard/android/shared/application_android.cc b/starboard/android/shared/application_android.cc index 61b7a8471db6..189517eab4b1 100644 --- a/starboard/android/shared/application_android.cc +++ b/starboard/android/shared/application_android.cc @@ -140,9 +140,15 @@ ApplicationAndroid::ApplicationAndroid(ALooper* looper) jobject local_ref = env->CallStarboardObjectMethodOrAbort( "getResourceOverlay", "()Ldev/cobalt/coat/ResourceOverlay;"); resource_overlay_ = env->ConvertLocalRefToGlobalRef(local_ref); + + env->CallStarboardVoidMethodOrAbort("starboardApplicationStarted", "()V"); } ApplicationAndroid::~ApplicationAndroid() { + // Inform StarboardBridge that + JniEnvExt* env = JniEnvExt::Get(); + env->CallStarboardVoidMethodOrAbort("starboardApplicationStopping", "()V"); + // The application is exiting. // Release the global reference. if (resource_overlay_) { diff --git a/starboard/android/shared/jni_env_ext.cc b/starboard/android/shared/jni_env_ext.cc index 06535d503b9a..98a65fdc49f5 100644 --- a/starboard/android/shared/jni_env_ext.cc +++ b/starboard/android/shared/jni_env_ext.cc @@ -72,7 +72,7 @@ void JniEnvExt::OnThreadShutdown() { } JniEnvExt* JniEnvExt::Get() { - JNIEnv* env; + JNIEnv* env = nullptr; if (JNI_OK != g_vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6)) { // Tell the JVM our thread name so it doesn't change it. char thread_name[16]; diff --git a/starboard/android/shared/video_window.cc b/starboard/android/shared/video_window.cc index a47cd54c1854..43b2d726ff82 100644 --- a/starboard/android/shared/video_window.cc +++ b/starboard/android/shared/video_window.cc @@ -135,20 +135,24 @@ void VideoSurfaceHolder::ClearVideoWindow(bool force_reset_surface) { return; } + JniEnvExt* env = JniEnvExt::Get(); + if (!env) { + SB_LOG(INFO) << "Tried to clear video window when JniEnvExt was null."; + return; + } + if (force_reset_surface) { - JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", - "()V"); + env->CallStarboardVoidMethodOrAbort("resetVideoSurface", "()V"); return; } else if (g_reset_surface_on_clear_window) { int width = ANativeWindow_getWidth(g_native_video_window); int height = ANativeWindow_getHeight(g_native_video_window); if (width <= height) { - JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("resetVideoSurface", - "()V"); + env->CallStarboardVoidMethodOrAbort("resetVideoSurface", "()V"); return; } } - JniEnvExt::Get()->CallStarboardVoidMethodOrAbort("clearVideoSurface", "()V"); + env->CallStarboardVoidMethodOrAbort("clearVideoSurface", "()V"); } } // namespace shared From 03d9995accf418ce4e5d6495d6aded5844fff830 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 5 Oct 2023 13:34:31 -0700 Subject: [PATCH 086/140] Cherry pick PR #1730: Revert "Clear video surface without re-initializing EGL. (#1413)" (#1732) Refer to the original PR: https://github.com/youtube/cobalt/pull/1730 This reverts commit 7eff4666ef9e3042b0841570e438218608624ea0. This reverts the switch from clearing the video window using low level EGL and GLES to using ClearVideoWindow because it appeared to have adverse effects on related video surface state management in the Android framework. b/302589325 b/303316973 Co-authored-by: Jelle Foks --- .../java/dev/cobalt/coat/CobaltActivity.java | 4 - .../java/dev/cobalt/coat/StarboardBridge.java | 9 -- .../dev/cobalt/media/VideoSurfaceView.java | 37 +---- starboard/android/shared/BUILD.gn | 2 +- starboard/android/shared/system_egl.cc | 146 ------------------ starboard/android/shared/video_window.cc | 100 ++++++++++-- 6 files changed, 90 insertions(+), 208 deletions(-) delete mode 100644 starboard/android/shared/system_egl.cc diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java index cb3a65b67c2d..2a763280dd1d 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java @@ -328,10 +328,6 @@ public void onRequestPermissionsResult( getStarboardBridge().onRequestPermissionsResult(requestCode, permissions, grantResults); } - public void clearVideoSurface() { - if (videoSurfaceView != null) videoSurfaceView.clearSurface(); - } - public void resetVideoSurface() { runOnUiThread( new Runnable() { diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java index dacb2dcc9b7a..d32cf59f29d7 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java @@ -712,15 +712,6 @@ void onRequestPermissionsResult(int requestCode, String[] permissions, int[] gra audioPermissionRequester.onRequestPermissionsResult(requestCode, permissions, grantResults); } - @SuppressWarnings("unused") - @UsedByNative - public void clearVideoSurface() { - Activity activity = activityHolder.get(); - if (activity instanceof CobaltActivity) { - ((CobaltActivity) activity).clearVideoSurface(); - } - } - @SuppressWarnings("unused") @UsedByNative public void resetVideoSurface() { diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java index 816c6495af14..a4ef40ea4b1f 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/VideoSurfaceView.java @@ -17,10 +17,7 @@ import static dev.cobalt.media.Log.TAG; import android.content.Context; -import android.graphics.Canvas; import android.graphics.Color; -import android.graphics.PixelFormat; -import android.graphics.PorterDuff; import android.os.Build; import android.util.AttributeSet; import android.view.Surface; @@ -37,7 +34,6 @@ public class VideoSurfaceView extends SurfaceView { private static Surface currentSurface = null; - private SurfaceHolder.Callback mSurfaceHolderCallback = null; private static final Set needResetSurfaceList = new HashSet<>(); @@ -72,46 +68,15 @@ public VideoSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, i private void initialize(Context context) { setBackgroundColor(Color.TRANSPARENT); - mSurfaceHolderCallback = new SurfaceHolderCallback(); - getHolder().addCallback(mSurfaceHolderCallback); + getHolder().addCallback(new SurfaceHolderCallback()); // TODO: Avoid recreating the surface when the player bounds change. // Recreating the surface is time-consuming and complicates synchronizing // punch-out video when the position / size is animated. } - public void clearSurface() { - SurfaceHolder holder = getHolder(); - if (holder == null) { - return; - } - Surface surface = holder.getSurface(); - if ((surface != null) && surface.isValid()) { - Canvas canvas = holder.lockCanvas(); - if (canvas != null) { - canvas.drawColor(Color.BLACK, PorterDuff.Mode.CLEAR); - holder.unlockCanvasAndPost(canvas); - } - } - // Trigger a surface changed event to prevent 'already connected'. - // But disable the callback to prevent it from making calls to the locking - // nativeOnVideoSurfaceChanged because we already are holding the same lock. - if (mSurfaceHolderCallback != null) { - holder.removeCallback(mSurfaceHolderCallback); - } - holder.setFormat(PixelFormat.TRANSPARENT); - holder.setFormat(PixelFormat.OPAQUE); - currentSurface = holder.getSurface(); - nativeOnVideoSurfaceChangedLocked(currentSurface); - if (mSurfaceHolderCallback != null) { - holder.addCallback(mSurfaceHolderCallback); - } - } - private static native void nativeOnVideoSurfaceChanged(Surface surface); - private static native void nativeOnVideoSurfaceChangedLocked(Surface surface); - private static native void nativeSetNeedResetSurface(); private class SurfaceHolderCallback implements SurfaceHolder.Callback { diff --git a/starboard/android/shared/BUILD.gn b/starboard/android/shared/BUILD.gn index 13585f33d31d..b07982aab008 100644 --- a/starboard/android/shared/BUILD.gn +++ b/starboard/android/shared/BUILD.gn @@ -52,6 +52,7 @@ action("game_activity_sources") { static_library("starboard_platform") { sources = [ + "//starboard/shared/egl/system_egl.cc", "//starboard/shared/gcc/atomic_gcc_public.h", "//starboard/shared/gles/gl_call.h", "//starboard/shared/gles/system_gles2.cc", @@ -364,7 +365,6 @@ static_library("starboard_platform") { "speech_synthesis_internal.cc", "speech_synthesis_is_supported.cc", "speech_synthesis_speak.cc", - "system_egl.cc", "system_get_connection_type.cc", "system_get_device_type.cc", "system_get_extensions.cc", diff --git a/starboard/android/shared/system_egl.cc b/starboard/android/shared/system_egl.cc deleted file mode 100644 index 9a81788e27bf..000000000000 --- a/starboard/android/shared/system_egl.cc +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2019 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. - -#include -#include -#include - -#include "starboard/egl.h" -#include "starboard/gles.h" - -#if !defined(EGL_VERSION_1_0) || !defined(EGL_VERSION_1_1) || \ - !defined(EGL_VERSION_1_2) || !defined(EGL_VERSION_1_3) || \ - !defined(EGL_VERSION_1_4) -#error "EGL version must be >= 1.4" -#endif - -namespace { - -bool first_make_current = false; - -EGLBoolean SbEglInitialize(EGLDisplay dpy, EGLint* major, EGLint* minor) { - first_make_current = true; - return eglInitialize(dpy, major, minor); -} - -EGLBoolean SbEglTerminate(EGLDisplay dpy) { - first_make_current = false; - return eglTerminate(dpy); -} - -EGLBoolean SbEglMakeCurrent(EGLDisplay dpy, - EGLSurface draw, - EGLSurface read, - EGLContext ctx) { - EGLBoolean result = eglMakeCurrent(dpy, draw, read, ctx); - if (first_make_current && (dpy != EGL_NO_DISPLAY) && - (draw != EGL_NO_SURFACE) && (eglGetError() == EGL_SUCCESS)) { - first_make_current = false; - // Start by showing a black surface immediately. - const SbGlesInterface* gles = SbGetGlesInterface(); - gles->glClearColor(0, 0, 0, 1); - gles->glClear(GL_COLOR_BUFFER_BIT); - gles->glFlush(); - if (glGetError() == GL_NO_ERROR) { - eglSwapBuffers(dpy, draw); - } - } - return result; -} - -// Convenience functions that redirect to the intended function but "cast" the -// type of the SbEglNative*Type parameter into the desired type. Depending on -// the platform, the type of cast to use is different so either C-style casts or -// constructor-style casts are needed to work across platforms (or provide -// implementations for these functions for each platform). - -SbEglBoolean SbEglCopyBuffers(SbEglDisplay dpy, - SbEglSurface surface, - SbEglNativePixmapType target) { - return eglCopyBuffers(dpy, surface, (EGLNativePixmapType)target); -} - -SbEglSurface SbEglCreatePixmapSurface(SbEglDisplay dpy, - SbEglConfig config, - SbEglNativePixmapType pixmap, - const SbEglInt32* attrib_list) { - return eglCreatePixmapSurface(dpy, config, (EGLNativePixmapType)pixmap, - attrib_list); -} - -SbEglSurface SbEglCreateWindowSurface(SbEglDisplay dpy, - SbEglConfig config, - SbEglNativeWindowType win, - const SbEglInt32* attrib_list) { - return eglCreateWindowSurface(dpy, config, (EGLNativeWindowType)win, - attrib_list); -} - -SbEglDisplay SbEglGetDisplay(SbEglNativeDisplayType display_id) { - return eglGetDisplay((EGLNativeDisplayType)display_id); -} - -const SbEglInterface g_sb_egl_interface = { - &eglChooseConfig, - &SbEglCopyBuffers, - &eglCreateContext, - &eglCreatePbufferSurface, - &SbEglCreatePixmapSurface, - &SbEglCreateWindowSurface, - &eglDestroyContext, - &eglDestroySurface, - &eglGetConfigAttrib, - &eglGetConfigs, - &eglGetCurrentDisplay, - &eglGetCurrentSurface, - &SbEglGetDisplay, - &eglGetError, - &eglGetProcAddress, - &SbEglInitialize, - &SbEglMakeCurrent, - &eglQueryContext, - &eglQueryString, - &eglQuerySurface, - &eglSwapBuffers, - &SbEglTerminate, - &eglWaitGL, - &eglWaitNative, - &eglBindTexImage, - &eglReleaseTexImage, - &eglSurfaceAttrib, - &eglSwapInterval, - &eglBindAPI, - &eglQueryAPI, - &eglCreatePbufferFromClientBuffer, - &eglReleaseThread, - &eglWaitClient, - &eglGetCurrentContext, - - nullptr, // eglCreateSync - nullptr, // eglDestroySync - nullptr, // eglClientWaitSync - nullptr, // eglGetSyncAttrib - nullptr, // eglCreateImage - nullptr, // eglDestroyImage - nullptr, // eglGetPlatformDisplay - nullptr, // eglCreatePlatformWindowSurface - nullptr, // eglCreatePlatformPixmapSurface - nullptr, // eglWaitSync -}; - -} // namespace - -const SbEglInterface* SbGetEglInterface() { - return &g_sb_egl_interface; -} diff --git a/starboard/android/shared/video_window.cc b/starboard/android/shared/video_window.cc index 43b2d726ff82..d6998e35793e 100644 --- a/starboard/android/shared/video_window.cc +++ b/starboard/android/shared/video_window.cc @@ -44,13 +44,98 @@ VideoSurfaceHolder* g_video_surface_holder = NULL; // Global boolean to indicate if we need to reset SurfaceView after playing // vertical video. bool g_reset_surface_on_clear_window = false; + +void ClearNativeWindow(ANativeWindow* native_window) { + EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY); + eglInitialize(display, NULL, NULL); + if (display == EGL_NO_DISPLAY) { + SB_DLOG(ERROR) << "Found no EGL display in ClearVideoWindow"; + return; + } + + const EGLint kAttributeList[] = { + EGL_RED_SIZE, + 8, + EGL_GREEN_SIZE, + 8, + EGL_BLUE_SIZE, + 8, + EGL_ALPHA_SIZE, + 8, + EGL_RENDERABLE_TYPE, + EGL_OPENGL_ES2_BIT, + EGL_NONE, + 0, + EGL_NONE, + }; + + // First, query how many configs match the given attribute list. + EGLint num_configs = 0; + EGL_CALL(eglChooseConfig(display, kAttributeList, NULL, 0, &num_configs)); + SB_DCHECK(num_configs != 0); + + // Allocate space to receive the matching configs and retrieve them. + EGLConfig* configs = new EGLConfig[num_configs]; + EGL_CALL(eglChooseConfig(display, kAttributeList, configs, num_configs, + &num_configs)); + + EGLNativeWindowType egl_native_window = + static_cast(native_window); + EGLConfig config; + + // Find the first config that successfully allow a window surface to be + // created. + EGLSurface surface; + for (int config_number = 0; config_number < num_configs; ++config_number) { + config = configs[config_number]; + surface = eglCreateWindowSurface(display, config, egl_native_window, NULL); + if (eglGetError() == EGL_SUCCESS) + break; + } + if (surface == EGL_NO_SURFACE) { + SB_DLOG(ERROR) << "Found no EGL surface in ClearVideoWindow"; + return; + } + SB_DCHECK(surface != EGL_NO_SURFACE); + + delete[] configs; + + // Create an OpenGL ES 2.0 context. + EGLContext context = EGL_NO_CONTEXT; + EGLint context_attrib_list[] = { + EGL_CONTEXT_CLIENT_VERSION, + 2, + EGL_NONE, + }; + context = + eglCreateContext(display, config, EGL_NO_CONTEXT, context_attrib_list); + SB_DCHECK(eglGetError() == EGL_SUCCESS); + SB_DCHECK(context != EGL_NO_CONTEXT); + + /* connect the context to the surface */ + EGL_CALL(eglMakeCurrent(display, surface, surface, context)); + + GL_CALL(glClearColor(0, 0, 0, 1)); + GL_CALL(glClear(GL_COLOR_BUFFER_BIT)); + GL_CALL(glFlush()); + EGL_CALL(eglSwapBuffers(display, surface)); + + // Cleanup all used resources. + EGL_CALL( + eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT)); + EGL_CALL(eglDestroyContext(display, context)); + EGL_CALL(eglDestroySurface(display, surface)); + EGL_CALL(eglTerminate(display)); +} + } // namespace extern "C" SB_EXPORT_PLATFORM void -Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChangedLocked( +Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged( JNIEnv* env, jobject unused_this, jobject surface) { + ScopedLock lock(*GetViewSurfaceMutex()); if (g_video_surface_holder) { g_video_surface_holder->OnSurfaceDestroyed(); g_video_surface_holder = NULL; @@ -66,19 +151,10 @@ Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChangedLocked( if (surface) { g_j_video_surface = env->NewGlobalRef(surface); g_native_video_window = ANativeWindow_fromSurface(env, surface); + ClearNativeWindow(g_native_video_window); } } -extern "C" SB_EXPORT_PLATFORM void -Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChanged( - JNIEnv* env, - jobject j_this, - jobject surface) { - ScopedLock lock(*GetViewSurfaceMutex()); - Java_dev_cobalt_media_VideoSurfaceView_nativeOnVideoSurfaceChangedLocked( - env, j_this, surface); -} - extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_media_VideoSurfaceView_nativeSetNeedResetSurface( JNIEnv* env, @@ -152,7 +228,7 @@ void VideoSurfaceHolder::ClearVideoWindow(bool force_reset_surface) { return; } } - env->CallStarboardVoidMethodOrAbort("clearVideoSurface", "()V"); + ClearNativeWindow(g_native_video_window); } } // namespace shared From d67cd169b2c847e66ab7d463adc0a5ad544f8078 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 5 Oct 2023 17:18:03 -0700 Subject: [PATCH 087/140] Cherry pick PR #1736: [android] Avoid exceptions in MediaCodecBridge.formatHasCropValues (#1739) Refer to the original PR: https://github.com/youtube/cobalt/pull/1736 This avoids possible NullPointerException and NoSuchElementException from MediaCodecBridge.formatHasCropValues. b/298692099 Co-authored-by: Jelle Foks --- .../src/main/java/dev/cobalt/media/MediaCodecBridge.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java index 8db14f68bee1..6d1c643d2e32 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java @@ -279,15 +279,15 @@ private GetOutputFormatResult() { } private boolean formatHasCropValues() { - if (!mFormatHasCropValues.isPresent()) { + if (!mFormatHasCropValues.isPresent() && mFormat != null) { boolean hasCropValues = mFormat.containsKey(KEY_CROP_RIGHT) && mFormat.containsKey(KEY_CROP_LEFT) && mFormat.containsKey(KEY_CROP_BOTTOM) && mFormat.containsKey(KEY_CROP_TOP); - mFormatHasCropValues = Optional.of(hasCropValues); + mFormatHasCropValues = Optional.ofNullable(hasCropValues); } - return mFormatHasCropValues.get(); + return mFormatHasCropValues.orElse(false); } @SuppressWarnings("unused") From 818e499aa26fb3a26718b586180f68153213e097 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 6 Oct 2023 14:46:51 -0700 Subject: [PATCH 088/140] Cherry pick PR #1738: [android] Check api level when log codec capabilities (#1740) Refer to the original PR: https://github.com/youtube/cobalt/pull/1738 b/302689405 Co-authored-by: xiaomings --- .../media/MediaCodecCapabilitiesLogger.java | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java index c30275d1c669..f1c640d2011a 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecCapabilitiesLogger.java @@ -20,6 +20,7 @@ import android.media.MediaCodecInfo.CodecCapabilities; import android.media.MediaCodecInfo.VideoCapabilities; import android.media.MediaCodecList; +import android.os.Build; import dev.cobalt.util.Log; import java.util.ArrayList; import java.util.Arrays; @@ -146,30 +147,38 @@ private static void ensurefeatureMapInitialized() { return codecCapabilities.isFeatureSupported( MediaCodecInfo.CodecCapabilities.FEATURE_AdaptivePlayback); }); - featureMap.put( - "FrameParsing", - (name, codecCapabilities) -> { - return codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_FrameParsing); - }); - featureMap.put( - "LowLatency", - (name, codecCapabilities) -> { - return codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency); - }); - featureMap.put( - "MultipleFrames", - (name, codecCapabilities) -> { - return codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_MultipleFrames); - }); - featureMap.put( - "PartialFrame", - (name, codecCapabilities) -> { - return codecCapabilities.isFeatureSupported( - MediaCodecInfo.CodecCapabilities.FEATURE_PartialFrame); - }); + if (Build.VERSION.SDK_INT >= 29) { + featureMap.put( + "FrameParsing", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_FrameParsing); + }); + } + if (Build.VERSION.SDK_INT >= 30) { + featureMap.put( + "LowLatency", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_LowLatency); + }); + } + if (Build.VERSION.SDK_INT >= 29) { + featureMap.put( + "MultipleFrames", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_MultipleFrames); + }); + } + if (Build.VERSION.SDK_INT >= 26) { + featureMap.put( + "PartialFrame", + (name, codecCapabilities) -> { + return codecCapabilities.isFeatureSupported( + MediaCodecInfo.CodecCapabilities.FEATURE_PartialFrame); + }); + } featureMap.put( "SecurePlayback", (name, codecCapabilities) -> { From a8d65805272a4439cb7d54f2998f2861f5e6c1ce Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 6 Oct 2023 15:14:19 -0700 Subject: [PATCH 089/140] Cherry pick PR #1733: [XB1] Remove arg introduced in python 3.8 (#1748) Refer to the original PR: https://github.com/youtube/cobalt/pull/1733 Remove arg that is introduced in python 3.8 as it's breaking our pylint for python 3.7, even though our builder is running python 3.11. This arg should be unneeded for our automated builds and was simply a convenience for manual testing. b/303258519 Change-Id: I414eabdd4a673fefa581bd6531830006303c9de0 Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/packager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starboard/xb1/tools/packager.py b/starboard/xb1/tools/packager.py index a8b4e5ef0f06..a5bcf6038f96 100644 --- a/starboard/xb1/tools/packager.py +++ b/starboard/xb1/tools/packager.py @@ -157,7 +157,7 @@ def _CopyAppxData(self): if not os.path.exists(source_dir): logging.error('Failed to find source content in: %s', source_dir) return - shutil.copytree(source_dir, appx_data_output_dir, dirs_exist_ok=True) + shutil.copytree(source_dir, appx_data_output_dir) @classmethod def SupportedPlatforms(cls): From 220c97e2255c2bd8918739c9128d4d7a6f130b1a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 9 Oct 2023 13:49:39 -0700 Subject: [PATCH 090/140] Cherry pick PR #1755: Default enable Client Hint Headers if not explicitly set (#1757) Refer to the original PR: https://github.com/youtube/cobalt/pull/1755 Kabuki has launched the experiment to 100%, so it should be safe to set this as default enabled for new installations. b/285656784 Co-authored-by: Garo Bournoutian --- cobalt/network/network_module.cc | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cobalt/network/network_module.cc b/cobalt/network/network_module.cc index 8154b7c8c7cc..b990b1d2a45d 100644 --- a/cobalt/network/network_module.cc +++ b/cobalt/network/network_module.cc @@ -112,10 +112,11 @@ void NetworkModule::SetEnableQuicFromPersistentSettings() { void NetworkModule::SetEnableClientHintHeadersFlagsFromPersistentSettings() { // Called on initialization and when the persistent setting is changed. + // If persistent setting is not set, will default to kCallTypeLoader. if (options_.persistent_settings != nullptr) { enable_client_hint_headers_flags_.store( options_.persistent_settings->GetPersistentSettingAsInt( - kClientHintHeadersEnabledPersistentSettingsKey, 0)); + kClientHintHeadersEnabledPersistentSettingsKey, kCallTypeLoader)); } } From 0469f8fe4d1ee3dd4cdbe7eb8f0883a087104699 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 9 Oct 2023 15:48:26 -0700 Subject: [PATCH 091/140] Cherry pick PR #1750: [media] Refine DCHECK() in OnDemuxerStreamRead() (#1759) Refer to the original PR: https://github.com/youtube/cobalt/pull/1750 The DCHECK(stream) in SbPlayerPipeline::OnDemuxerStreamRead() may trigger when SbPlayerPipeline::Stop() has been called. It won't affect production build as it's a DCHECK and the check below on |player_bridge_| correctly guarded the code from a stopped pipeline. Now the check is made explicit, and redundant DCHECK() and if statements are added. b/276483058 Co-authored-by: xiaomings --- cobalt/media/base/sbplayer_pipeline.cc | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/cobalt/media/base/sbplayer_pipeline.cc b/cobalt/media/base/sbplayer_pipeline.cc index 55bf81865051..8b2962832fff 100644 --- a/cobalt/media/base/sbplayer_pipeline.cc +++ b/cobalt/media/base/sbplayer_pipeline.cc @@ -1010,12 +1010,17 @@ void SbPlayerPipeline::OnDemuxerStreamRead( return; } + if (stopped_) { + return; + } + + DCHECK(player_bridge_); + DemuxerStream* stream = type == DemuxerStream::AUDIO ? audio_stream_ : video_stream_; DCHECK(stream); - // In case if Stop() has been called. - if (!player_bridge_) { + if (!player_bridge_ || !stream) { return; } From 48078592a9606ef9eda8135c04ede41fbf342973 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 9 Oct 2023 17:55:38 -0700 Subject: [PATCH 092/140] Cherry pick PR #1729: Bump SB_MINIMUM_API_VERSION from 12 to 13 (#1758) Refer to the original PR: https://github.com/youtube/cobalt/pull/1729 I think trunk should only support 13, 14, 15, and 16. b/283015285 Change-Id: I75216e798b8ebfb6d3f5b1a8547156a980509e74 Co-authored-by: Holden Warriner --- starboard/configuration.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/starboard/configuration.h b/starboard/configuration.h index 651f13161817..da4eff5e8316 100644 --- a/starboard/configuration.h +++ b/starboard/configuration.h @@ -35,7 +35,7 @@ // The minimum API version allowed by this version of the Starboard headers, // inclusive. -#define SB_MINIMUM_API_VERSION 12 +#define SB_MINIMUM_API_VERSION 13 // The maximum API version allowed by this version of the Starboard headers, // inclusive. From db9ae99e8fafa473ef5de04c97dc11da2bf116f0 Mon Sep 17 00:00:00 2001 From: Kaido Kert Date: Tue, 10 Oct 2023 14:05:19 -0700 Subject: [PATCH 093/140] Update LTS minor version to 13 (#1769) b/260110906 --- cobalt/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/version.h b/cobalt/version.h index cdce9e2579c6..9911a87f4040 100644 --- a/cobalt/version.h +++ b/cobalt/version.h @@ -35,6 +35,6 @@ // release is cut. //. -#define COBALT_VERSION "24.lts.12" +#define COBALT_VERSION "24.lts.13" #endif // COBALT_VERSION_H_ From 77e9bba13c562d841c074a81ed28533dd3e8084a Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 10 Oct 2023 16:19:11 -0700 Subject: [PATCH 094/140] Cherry pick PR #1760: Add start application state to crash reports. (#1766) Refer to the original PR: https://github.com/youtube/cobalt/pull/1760 b/302372195 Change-Id: I6ee7f6b5978b9e17d1e2c28c5a1a42f376204948 Co-authored-by: Brian Ting --- cobalt/browser/application.cc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc index 31cb00dfe339..7391d1d88228 100644 --- a/cobalt/browser/application.cc +++ b/cobalt/browser/application.cc @@ -1025,6 +1025,8 @@ Application::Application(const base::Closure& quit_closure, bool should_preload, base::TimeDelta::FromSeconds(duration_in_seconds)); } #endif // ENABLE_DEBUG_COMMAND_LINE_SWITCHES + + AddCrashLogApplicationState(base::kApplicationStateStarted); } Application::~Application() { From 5a3346560891861bcf9708c5f2c2bf94fa1bb085 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 10 Oct 2023 17:10:15 -0700 Subject: [PATCH 095/140] Cherry pick PR #1761: Support DVLOG() via --min_log_level=verbose (#1774) Refer to the original PR: https://github.com/youtube/cobalt/pull/1761 It sets the log level to `LOG_VERBOSE - 15` to effectively enables all known verbose messages. Supporting detailed verbose level is possible but not implemented so the implementation remains straight forward. b/276483058 Co-authored-by: xiaomings --- base/logging.cc | 8 +++++++- cobalt/browser/application.cc | 7 ++++++- cobalt/browser/switches.cc | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/base/logging.cc b/base/logging.cc index 809e14119fa2..5a159ff4f3a0 100644 --- a/base/logging.cc +++ b/base/logging.cc @@ -478,9 +478,15 @@ SbLogPriority LogLevelToStarboardLogPriority(int level) { case LOG_ERROR: return kSbLogPriorityError; case LOG_FATAL: - case LOG_VERBOSE: return kSbLogPriorityFatal; + case LOG_VERBOSE: default: + if (level <= LOG_VERBOSE) { + // Verbose level can be any negative integer, sanity check its range to + // filter out potential errors. + DCHECK_GE(level, -256); + return kSbLogPriorityInfo; + } NOTREACHED() << "Unrecognized log level."; return kSbLogPriorityInfo; } diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc index 7391d1d88228..ba786739088c 100644 --- a/cobalt/browser/application.cc +++ b/cobalt/browser/application.cc @@ -460,7 +460,12 @@ std::string GetMinLogLevelString() { } int StringToLogLevel(const std::string& log_level) { - if (log_level == "info") { + if (log_level == "verbose") { + // The lower the verbose level is, the more messages are logged. Set it to + // a lower enough value to ensure that all known verbose messages are + // logged. + return logging::LOG_VERBOSE - 15; + } else if (log_level == "info") { return logging::LOG_INFO; } else if (log_level == "warning") { return logging::LOG_WARNING; diff --git a/cobalt/browser/switches.cc b/cobalt/browser/switches.cc index 70bb7561cd1a..11c7db645da3 100644 --- a/cobalt/browser/switches.cc +++ b/cobalt/browser/switches.cc @@ -229,7 +229,7 @@ const char kWebDriverPortHelp[] = const char kMinLogLevel[] = "min_log_level"; const char kMinLogLevelHelp[] = - "Set the minimum logging level: info|warning|error|fatal."; + "Set the minimum logging level: verbose|info|warning|error|fatal."; const char kDisableJavaScriptJit[] = "disable_javascript_jit"; const char kDisableJavaScriptJitHelp[] = "Specifies that javascript jit should be disabled."; From 014db50e0051b7fa6c1ec344ff3f96ea4d039429 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 10 Oct 2023 21:40:38 -0700 Subject: [PATCH 096/140] Cherry pick PR #1771: [XB1] Remove IAudioEndpointVolume API (#1777) Refer to the original PR: https://github.com/youtube/cobalt/pull/1771 Replaces the IAudioEndpointVolume API with ISimpleAudioVolume ahead of a future API change that will remove IAudioEndpointVolume functionality. b/303500203 Co-authored-by: Austin Osagie --- starboard/shared/uwp/wasapi_audio_sink.cc | 8 ++++---- starboard/shared/uwp/wasapi_audio_sink.h | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/starboard/shared/uwp/wasapi_audio_sink.cc b/starboard/shared/uwp/wasapi_audio_sink.cc index 2beffa19ce9d..d8f5990b572e 100644 --- a/starboard/shared/uwp/wasapi_audio_sink.cc +++ b/starboard/shared/uwp/wasapi_audio_sink.cc @@ -97,9 +97,9 @@ bool WASAPIAudioSink::Initialize(int channels, return false; } - hr = device_->Activate( - IID_IAudioEndpointVolume, CLSCTX_ALL, NULL, - reinterpret_cast(audio_endpoint_volume_.GetAddressOf())); + hr = audio_client_->GetService( + IID_ISimpleAudioVolume, + reinterpret_cast(audio_volume_.GetAddressOf())); if (hr != S_OK) { SB_LOG(ERROR) << "Failed to initialize volume handler, error code: " << std::hex << hr; @@ -294,7 +294,7 @@ void WASAPIAudioSink::UpdatePlaybackState() { } double volume = volume_.load(); if (current_volume_ != volume) { - hr = audio_endpoint_volume_->SetMasterVolumeLevelScalar(volume, NULL); + hr = audio_volume_->SetMasterVolume(volume, NULL); CHECK_HRESULT_OK(hr); current_volume_ = volume; } diff --git a/starboard/shared/uwp/wasapi_audio_sink.h b/starboard/shared/uwp/wasapi_audio_sink.h index 86863a41102a..3d94f5329bb6 100644 --- a/starboard/shared/uwp/wasapi_audio_sink.h +++ b/starboard/shared/uwp/wasapi_audio_sink.h @@ -16,11 +16,9 @@ #define STARBOARD_SHARED_UWP_WASAPI_AUDIO_SINK_H_ #include -#include #include #include -#include #include #include @@ -119,7 +117,6 @@ IMMDeviceEnumerator : public IUnknown { }; const IID IID_IAudioClock = __uuidof(IAudioClock); -const IID IID_IAudioEndpointVolume = __uuidof(IAudioEndpointVolume); const IID IID_IAudioRenderClient = __uuidof(IAudioRenderClient); const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator); const IID IID_ISimpleAudioVolume = __uuidof(ISimpleAudioVolume); @@ -168,7 +165,7 @@ class WASAPIAudioSink { Microsoft::WRL::ComPtr device_; Microsoft::WRL::ComPtr audio_client_; Microsoft::WRL::ComPtr render_client_; - Microsoft::WRL::ComPtr audio_endpoint_volume_; + Microsoft::WRL::ComPtr audio_volume_; Mutex audio_clock_mutex_; Microsoft::WRL::ComPtr audio_clock_; From 595a8e9682fc98f124dce4d6ef774f9226967a8f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 11 Oct 2023 13:03:09 -0700 Subject: [PATCH 097/140] Cherry pick PR #1778: Add Client Hint Headers to XHR requests by default (#1782) Refer to the original PR: https://github.com/youtube/cobalt/pull/1778 b/285656784 Co-authored-by: Garo Bournoutian --- cobalt/network/network_module.cc | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cobalt/network/network_module.cc b/cobalt/network/network_module.cc index b990b1d2a45d..8f562ea882e6 100644 --- a/cobalt/network/network_module.cc +++ b/cobalt/network/network_module.cc @@ -112,11 +112,13 @@ void NetworkModule::SetEnableQuicFromPersistentSettings() { void NetworkModule::SetEnableClientHintHeadersFlagsFromPersistentSettings() { // Called on initialization and when the persistent setting is changed. - // If persistent setting is not set, will default to kCallTypeLoader. + // If persistent setting is not set, will default to + // kCallTypeLoader | kCallTypeXHR. if (options_.persistent_settings != nullptr) { enable_client_hint_headers_flags_.store( options_.persistent_settings->GetPersistentSettingAsInt( - kClientHintHeadersEnabledPersistentSettingsKey, kCallTypeLoader)); + kClientHintHeadersEnabledPersistentSettingsKey, + (kCallTypeLoader | kCallTypeXHR))); } } From f57fefe570898481c5739546bda83baac06c08fa Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 11 Oct 2023 15:40:56 -0700 Subject: [PATCH 098/140] Cherry pick PR #1783: Fix telemetry native crash (#1786) Refer to the original PR: https://github.com/youtube/cobalt/pull/1783 Instead of storing a reference to a JS callback in the metrics_log_uploader, we instead publish an event to EventDispatcher when metrics are ready to be uploaded. H5vccMetrics listens for these events and returns the result to the JS callback. This eliminates race conditions where the JS environment (and therefore H5vcc) was being destroyed and metrics were queued for upload, referencing freed pointers. b/298050585 Change-Id: I261436a00099d7640791bc3a9ec93aab600ed3db Co-authored-by: Joel Martinez --- cobalt/base/BUILD.gn | 1 + cobalt/base/on_metric_upload_event.h | 61 +++++++++++++++++++ cobalt/browser/application.cc | 1 + cobalt/browser/metrics/BUILD.gn | 5 +- .../metrics/cobalt_metrics_log_uploader.cc | 25 ++++---- .../metrics/cobalt_metrics_log_uploader.h | 12 ++-- .../cobalt_metrics_log_uploader_test.cc | 46 ++++++++------ .../metrics/cobalt_metrics_service_client.cc | 25 +++----- .../metrics/cobalt_metrics_service_client.h | 17 ++---- .../cobalt_metrics_services_manager.cc | 37 +++-------- .../metrics/cobalt_metrics_services_manager.h | 23 ++----- .../cobalt_metrics_uploader_callback.h | 36 ----------- cobalt/h5vcc/h5vcc.cc | 3 +- cobalt/h5vcc/h5vcc_metrics.cc | 43 +++++++------ cobalt/h5vcc/h5vcc_metrics.h | 22 ++++--- 15 files changed, 180 insertions(+), 177 deletions(-) create mode 100644 cobalt/base/on_metric_upload_event.h delete mode 100644 cobalt/browser/metrics/cobalt_metrics_uploader_callback.h diff --git a/cobalt/base/BUILD.gn b/cobalt/base/BUILD.gn index a6190034f0aa..f2652cc1255d 100644 --- a/cobalt/base/BUILD.gn +++ b/cobalt/base/BUILD.gn @@ -60,6 +60,7 @@ static_library("base") { "log_message_handler.cc", "log_message_handler.h", "message_queue.h", + "on_metric_upload_event.h", "on_screen_keyboard_hidden_event.h", "on_screen_keyboard_shown_event.h", "path_provider.cc", diff --git a/cobalt/base/on_metric_upload_event.h b/cobalt/base/on_metric_upload_event.h new file mode 100644 index 000000000000..d011f0a8ee2e --- /dev/null +++ b/cobalt/base/on_metric_upload_event.h @@ -0,0 +1,61 @@ +// Copyright 2023 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 COBALT_BASE_ON_METRIC_UPLOAD_EVENT_H_ +#define COBALT_BASE_ON_METRIC_UPLOAD_EVENT_H_ + +#include +#include + +#include "base/callback.h" +#include "base/compiler_specific.h" +#include "base/strings/string_util.h" +#include "cobalt/base/event.h" +#include "cobalt/base/polymorphic_downcast.h" +#include "cobalt/h5vcc/h5vcc_metric_type.h" + +namespace base { + +// Event sent when a metric payload is ready for upload. +class OnMetricUploadEvent : public Event { + public: + OnMetricUploadEvent(const cobalt::h5vcc::H5vccMetricType& metric_type, + const std::string& serialized_proto) + : metric_type_(metric_type), serialized_proto_(serialized_proto) {} + + explicit OnMetricUploadEvent(const Event* event) { + CHECK(event != nullptr); + const base::OnMetricUploadEvent* on_metric_upload_event = + base::polymorphic_downcast(event); + metric_type_ = on_metric_upload_event->metric_type(); + serialized_proto_ = on_metric_upload_event->serialized_proto(); + } + + const cobalt::h5vcc::H5vccMetricType& metric_type() const { + return metric_type_; + } + + const std::string& serialized_proto() const { return serialized_proto_; } + + + BASE_EVENT_SUBCLASS(OnMetricUploadEvent); + + private: + cobalt::h5vcc::H5vccMetricType metric_type_; + std::string serialized_proto_; +}; + +} // namespace base + +#endif // COBALT_BASE_ON_METRIC_UPLOAD_EVENT_H_ diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc index ba786739088c..117452f99778 100644 --- a/cobalt/browser/application.cc +++ b/cobalt/browser/application.cc @@ -1531,6 +1531,7 @@ void Application::InitMetrics() { metrics::kMetricEnabledSettingName, false); auto metric_event_interval = persistent_settings_->GetPersistentSettingAsInt( metrics::kMetricEventIntervalSettingName, 300); + metrics_services_manager_->SetEventDispatcher(&event_dispatcher_); metrics_services_manager_->SetUploadInterval(metric_event_interval); metrics_services_manager_->ToggleMetricsEnabled(is_metrics_enabled); // Metric recording state initialization _must_ happen before we bootstrap diff --git a/cobalt/browser/metrics/BUILD.gn b/cobalt/browser/metrics/BUILD.gn index 62c85fa5e558..414d1b506573 100644 --- a/cobalt/browser/metrics/BUILD.gn +++ b/cobalt/browser/metrics/BUILD.gn @@ -24,11 +24,11 @@ static_library("metrics") { "cobalt_metrics_services_manager.h", "cobalt_metrics_services_manager_client.cc", "cobalt_metrics_services_manager_client.h", - "cobalt_metrics_uploader_callback.h", ] deps = [ "//base", + "//cobalt/base", "//cobalt/browser:generated_types", "//cobalt/h5vcc:metric_event_handler_wrapper", "//components/metrics", @@ -51,8 +51,9 @@ target(gtest_target_type, "metrics_test") { deps = [ ":metrics", "//base", - "//cobalt//browser:test_dependencies_on_browser", + "//cobalt/base", "//cobalt/browser:generated_types", + "//cobalt/browser:test_dependencies_on_browser", "//cobalt/h5vcc", "//cobalt/h5vcc:metric_event_handler_wrapper", "//cobalt/test:run_all_unittests", diff --git a/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc b/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc index 2c8088377ec8..955ef04277fc 100644 --- a/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc +++ b/cobalt/browser/metrics/cobalt_metrics_log_uploader.cc @@ -14,9 +14,12 @@ #include "cobalt/browser/metrics/cobalt_metrics_log_uploader.h" +#include + #include "base/base64url.h" #include "base/logging.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" +#include "cobalt/base/event_dispatcher.h" +#include "cobalt/base/on_metric_upload_event.h" #include "cobalt/h5vcc/h5vcc_metric_type.h" #include "components/metrics/log_decoder.h" #include "components/metrics/metrics_log_uploader.h" @@ -49,7 +52,7 @@ void CobaltMetricsLogUploader::UploadLog( const std::string& compressed_log_data, const std::string& log_hash, const ::metrics::ReportingInfo& reporting_info) { if (service_type_ == ::metrics::MetricsLogUploader::UMA) { - if (upload_handler_ != nullptr) { + if (event_dispatcher_ != nullptr) { std::string uncompressed_serialized_proto; ::metrics::DecodeLogData(compressed_log_data, &uncompressed_serialized_proto); @@ -67,13 +70,11 @@ void CobaltMetricsLogUploader::UploadLog( base::Base64UrlEncode(cobalt_uma_event.SerializeAsString(), base::Base64UrlEncodePolicy::INCLUDE_PADDING, &base64_encoded_proto); - // Check again that the upload handler is still valid. Was seeing race - // conditions where it was being destroyed while the proto encoding was - // happening above. - if (upload_handler_ != nullptr) { - upload_handler_->Run(h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma, - base64_encoded_proto); - } + + event_dispatcher_->DispatchEvent( + std::unique_ptr(new base::OnMetricUploadEvent( + h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma, + base64_encoded_proto))); } } @@ -84,9 +85,9 @@ void CobaltMetricsLogUploader::UploadLog( /*was_https*/ true); } -void CobaltMetricsLogUploader::SetOnUploadHandler( - const CobaltMetricsUploaderCallback* upload_handler) { - upload_handler_ = upload_handler; +void CobaltMetricsLogUploader::SetEventDispatcher( + const base::EventDispatcher* event_dispatcher) { + event_dispatcher_ = event_dispatcher; } } // namespace metrics diff --git a/cobalt/browser/metrics/cobalt_metrics_log_uploader.h b/cobalt/browser/metrics/cobalt_metrics_log_uploader.h index fa71662ab5e6..8b5b73dafd80 100644 --- a/cobalt/browser/metrics/cobalt_metrics_log_uploader.h +++ b/cobalt/browser/metrics/cobalt_metrics_log_uploader.h @@ -20,7 +20,7 @@ #include "base/callback.h" #include "base/macros.h" #include "base/strings/string_piece.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" +#include "cobalt/base/event_dispatcher.h" #include "cobalt/h5vcc/metric_event_handler_wrapper.h" #include "components/metrics/metrics_log_uploader.h" #include "third_party/metrics_proto/reporting_info.pb.h" @@ -43,6 +43,9 @@ class CobaltMetricsLogUploader : public ::metrics::MetricsLogUploader { virtual ~CobaltMetricsLogUploader() {} + // Set event dispatcher to be used to publish any metrics events (eg upload). + void SetEventDispatcher(const base::EventDispatcher* event_dispatcher); + // Uploads a log with the specified |compressed_log_data| and |log_hash|. // |log_hash| is expected to be the hex-encoded SHA1 hash of the log data // before compression. @@ -50,15 +53,10 @@ class CobaltMetricsLogUploader : public ::metrics::MetricsLogUploader { const std::string& log_hash, const ::metrics::ReportingInfo& reporting_info); - // Sets the event handler wrapper to be called when metrics are ready for - // upload. This should be the JavaScript H5vcc callback implementation. - void SetOnUploadHandler( - const CobaltMetricsUploaderCallback* metric_event_handler); - private: const ::metrics::MetricsLogUploader::MetricServiceType service_type_; const ::metrics::MetricsLogUploader::UploadCallback on_upload_complete_; - const CobaltMetricsUploaderCallback* upload_handler_ = nullptr; + const base::EventDispatcher* event_dispatcher_ = nullptr; }; } // namespace metrics diff --git a/cobalt/browser/metrics/cobalt_metrics_log_uploader_test.cc b/cobalt/browser/metrics/cobalt_metrics_log_uploader_test.cc index 78e7d186bf70..f6927a861a2b 100644 --- a/cobalt/browser/metrics/cobalt_metrics_log_uploader_test.cc +++ b/cobalt/browser/metrics/cobalt_metrics_log_uploader_test.cc @@ -18,7 +18,10 @@ #include "base/base64url.h" #include "base/test/mock_callback.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" +#include "cobalt/base/event.h" +#include "cobalt/base/event_dispatcher.h" +#include "cobalt/base/on_metric_upload_event.h" +#include "cobalt/h5vcc/h5vcc_metric_type.h" #include "cobalt/h5vcc/h5vcc_metrics.h" #include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" @@ -26,7 +29,6 @@ #include "third_party/metrics_proto/cobalt_uma_event.pb.h" #include "third_party/metrics_proto/reporting_info.pb.h" #include "third_party/zlib/google/compression_utils.h" - namespace cobalt { namespace browser { namespace metrics { @@ -43,6 +45,12 @@ using ::testing::StrictMock; class CobaltMetricsLogUploaderTest : public ::testing::Test { public: void SetUp() override { + dispatcher_ = std::make_unique(); + dispatcher_->AddEventCallback( + base::OnMetricUploadEvent::TypeId(), + base::Bind(&CobaltMetricsLogUploaderTest::OnMetricUploadEventHandler, + base::Unretained(this))); + uploader_ = std::make_unique( ::metrics::MetricsLogUploader::MetricServiceType::UMA, base::Bind(&CobaltMetricsLogUploaderTest::UploadCompleteCallback, @@ -51,6 +59,13 @@ class CobaltMetricsLogUploaderTest : public ::testing::Test { void TearDown() override {} + void OnMetricUploadEventHandler(const base::Event* event) { + std::unique_ptr on_metric_upload_event( + new base::OnMetricUploadEvent(event)); + last_metric_type_ = on_metric_upload_event->metric_type(); + last_serialized_proto_ = on_metric_upload_event->serialized_proto(); + } + void UploadCompleteCallback(int response_code, int error_code, bool was_https) { callback_count_++; @@ -58,13 +73,14 @@ class CobaltMetricsLogUploaderTest : public ::testing::Test { protected: std::unique_ptr uploader_; + std::unique_ptr dispatcher_; int callback_count_ = 0; + cobalt::h5vcc::H5vccMetricType last_metric_type_; + std::string last_serialized_proto_ = ""; }; TEST_F(CobaltMetricsLogUploaderTest, TriggersUploadHandler) { - base::MockCallback mock_upload_handler; - const auto cb = mock_upload_handler.Get(); - uploader_->SetOnUploadHandler(&cb); + uploader_->SetEventDispatcher(dispatcher_.get()); ::metrics::ReportingInfo dummy_reporting_info; dummy_reporting_info.set_attempt_count(33); ::metrics::ChromeUserMetricsExtension uma_log; @@ -87,12 +103,11 @@ TEST_F(CobaltMetricsLogUploaderTest, TriggersUploadHandler) { base::Base64UrlEncode(cobalt_event.SerializeAsString(), base::Base64UrlEncodePolicy::INCLUDE_PADDING, &base64_encoded_proto); - EXPECT_CALL(mock_upload_handler, - Run(Eq(h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma), - StrEq(base64_encoded_proto))) - .Times(1); uploader_->UploadLog(compressed_message, "fake_hash", dummy_reporting_info); ASSERT_EQ(callback_count_, 1); + ASSERT_EQ(last_metric_type_, + cobalt::h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma); + ASSERT_EQ(last_serialized_proto_, base64_encoded_proto); ::metrics::ChromeUserMetricsExtension uma_log2; uma_log2.set_session_id(456); @@ -108,11 +123,10 @@ TEST_F(CobaltMetricsLogUploaderTest, TriggersUploadHandler) { base::Base64UrlEncode(cobalt_event2.SerializeAsString(), base::Base64UrlEncodePolicy::INCLUDE_PADDING, &base64_encoded_proto2); - EXPECT_CALL(mock_upload_handler, - Run(Eq(h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma), - StrEq(base64_encoded_proto2))) - .Times(1); uploader_->UploadLog(compressed_message2, "fake_hash", dummy_reporting_info); + ASSERT_EQ(last_metric_type_, + cobalt::h5vcc::H5vccMetricType::kH5vccMetricTypeCobaltUma); + ASSERT_EQ(last_serialized_proto_, base64_encoded_proto2); ASSERT_EQ(callback_count_, 2); } @@ -121,17 +135,15 @@ TEST_F(CobaltMetricsLogUploaderTest, UnknownMetricTypeDoesntTriggerUpload) { ::metrics::MetricsLogUploader::MetricServiceType::UKM, base::Bind(&CobaltMetricsLogUploaderTest::UploadCompleteCallback, base::Unretained(this)))); - base::MockCallback mock_upload_handler; - const auto cb = mock_upload_handler.Get(); - uploader_->SetOnUploadHandler(&cb); + uploader_->SetEventDispatcher(dispatcher_.get()); ::metrics::ReportingInfo dummy_reporting_info; ::metrics::ChromeUserMetricsExtension uma_log; uma_log.set_session_id(1234); uma_log.set_client_id(1234); std::string compressed_message; compression::GzipCompress(uma_log.SerializeAsString(), &compressed_message); - EXPECT_CALL(mock_upload_handler, Run(_, _)).Times(0); uploader_->UploadLog(compressed_message, "fake_hash", dummy_reporting_info); + ASSERT_EQ(last_serialized_proto_, ""); // Even though we don't upload this log, we still need to trigger the complete // callback so the metric code can keep running. ASSERT_EQ(callback_count_, 1); diff --git a/cobalt/browser/metrics/cobalt_metrics_service_client.cc b/cobalt/browser/metrics/cobalt_metrics_service_client.cc index 5856ce6b19e5..c3408a96eec8 100644 --- a/cobalt/browser/metrics/cobalt_metrics_service_client.cc +++ b/cobalt/browser/metrics/cobalt_metrics_service_client.cc @@ -24,9 +24,9 @@ #include "base/memory/singleton.h" #include "base/strings/string16.h" #include "base/time/time.h" +#include "cobalt/base/event_dispatcher.h" #include "cobalt/browser/metrics/cobalt_enabled_state_provider.h" #include "cobalt/browser/metrics/cobalt_metrics_log_uploader.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" #include "components/metrics/enabled_state_provider.h" #include "components/metrics/metrics_log_uploader.h" #include "components/metrics/metrics_pref_names.h" @@ -50,22 +50,11 @@ namespace metrics { // Upload Handler. const int kStandardUploadIntervalSeconds = 5 * 60; // 5 minutes. -void CobaltMetricsServiceClient::SetOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback) { - upload_handler_ = uploader_callback; +void CobaltMetricsServiceClient::SetEventDispatcher( + const base::EventDispatcher* event_dispatcher) { + event_dispatcher_ = event_dispatcher; if (log_uploader_) { - log_uploader_->SetOnUploadHandler(upload_handler_); - } -} - -void CobaltMetricsServiceClient::RemoveOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback) { - // Only remove the upload handler if our current reference matches that which - // is passed in. Avoids issues with race conditions with two threads trying to - // override the handler. - if (upload_handler_ == uploader_callback) { - LOG(INFO) << "Upload handler removed."; - upload_handler_ = nullptr; + log_uploader_->SetEventDispatcher(event_dispatcher); } } @@ -165,8 +154,8 @@ CobaltMetricsServiceClient::CreateUploader( auto uploader = std::make_unique( service_type, on_upload_complete); log_uploader_ = uploader.get(); - if (upload_handler_ != nullptr) { - log_uploader_->SetOnUploadHandler(upload_handler_); + if (event_dispatcher_ != nullptr) { + log_uploader_->SetEventDispatcher(event_dispatcher_); } return uploader; } diff --git a/cobalt/browser/metrics/cobalt_metrics_service_client.h b/cobalt/browser/metrics/cobalt_metrics_service_client.h index 7ba9a7cd9df5..c8efa8f979ba 100644 --- a/cobalt/browser/metrics/cobalt_metrics_service_client.h +++ b/cobalt/browser/metrics/cobalt_metrics_service_client.h @@ -23,9 +23,9 @@ #include "base/callback.h" #include "base/strings/string16.h" #include "base/time/time.h" +#include "cobalt/base/event_dispatcher.h" #include "cobalt/browser/metrics/cobalt_enabled_state_provider.h" #include "cobalt/browser/metrics/cobalt_metrics_log_uploader.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" #include "components/metrics/metrics_log_uploader.h" #include "components/metrics/metrics_reporting_default_state.h" #include "components/metrics/metrics_service.h" @@ -48,15 +48,8 @@ class CobaltMetricsServiceClient : public ::metrics::MetricsServiceClient { public: ~CobaltMetricsServiceClient() override{}; - // Sets the uploader handler to be called when metrics are ready for - // upload. - void SetOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback); - - // Remove reference to the passed uploader callback, if it's the current - // reference. Otherwise, does nothing. - void RemoveOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback); + // Set event dispatcher to be used to publish any metrics events (eg upload). + void SetEventDispatcher(const base::EventDispatcher* event_dispatcher); // Returns the MetricsService instance that this client is associated with. // With the exception of testing contexts, the returned instance must be valid @@ -170,10 +163,10 @@ class CobaltMetricsServiceClient : public ::metrics::MetricsServiceClient { CobaltMetricsLogUploader* log_uploader_ = nullptr; - const CobaltMetricsUploaderCallback* upload_handler_ = nullptr; - uint32_t custom_upload_interval_ = UINT32_MAX; + const base::EventDispatcher* event_dispatcher_ = nullptr; + DISALLOW_COPY_AND_ASSIGN(CobaltMetricsServiceClient); }; diff --git a/cobalt/browser/metrics/cobalt_metrics_services_manager.cc b/cobalt/browser/metrics/cobalt_metrics_services_manager.cc index a17f7b0d1fbe..f81685849401 100644 --- a/cobalt/browser/metrics/cobalt_metrics_services_manager.cc +++ b/cobalt/browser/metrics/cobalt_metrics_services_manager.cc @@ -17,6 +17,7 @@ #include #include "base/logging.h" +#include "cobalt/base/event_dispatcher.h" #include "cobalt/browser/metrics/cobalt_metrics_service_client.h" #include "cobalt/browser/metrics/cobalt_metrics_services_manager_client.h" #include "components/metrics_services_manager/metrics_services_manager.h" @@ -46,44 +47,22 @@ void CobaltMetricsServicesManager::DeleteInstance() { instance_ = nullptr; } -void CobaltMetricsServicesManager::RemoveOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback) { +void CobaltMetricsServicesManager::SetEventDispatcher( + base::EventDispatcher* event_dispatcher) { if (instance_ != nullptr) { instance_->task_runner_->PostTask( FROM_HERE, - base::Bind(&CobaltMetricsServicesManager::RemoveOnUploadHandlerInternal, - base::Unretained(instance_), uploader_callback)); + base::Bind(&CobaltMetricsServicesManager::SetEventDispatcherInternal, + base::Unretained(instance_), event_dispatcher)); } } -void CobaltMetricsServicesManager::RemoveOnUploadHandlerInternal( - const CobaltMetricsUploaderCallback* uploader_callback) { +void CobaltMetricsServicesManager::SetEventDispatcherInternal( + base::EventDispatcher* event_dispatcher) { CobaltMetricsServiceClient* client = static_cast(GetMetricsServiceClient()); DCHECK(client); - client->RemoveOnUploadHandler(uploader_callback); -} - -void CobaltMetricsServicesManager::SetOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback) { - // H5vccMetrics calls this on destruction when the WebModule is torn down. On - // shutdown, CobaltMetricsServicesManager can be destructed before - // H5vccMetrics, so we make sure we have a valid instance here. - if (instance_ != nullptr) { - instance_->task_runner_->PostTask( - FROM_HERE, - base::Bind(&CobaltMetricsServicesManager::SetOnUploadHandlerInternal, - base::Unretained(instance_), uploader_callback)); - } -} - -void CobaltMetricsServicesManager::SetOnUploadHandlerInternal( - const CobaltMetricsUploaderCallback* uploader_callback) { - CobaltMetricsServiceClient* client = - static_cast(GetMetricsServiceClient()); - DCHECK(client); - client->SetOnUploadHandler(uploader_callback); - LOG(INFO) << "New Cobalt Telemetry metric upload handler bound."; + client->SetEventDispatcher(event_dispatcher); } void CobaltMetricsServicesManager::ToggleMetricsEnabled(bool is_enabled) { diff --git a/cobalt/browser/metrics/cobalt_metrics_services_manager.h b/cobalt/browser/metrics/cobalt_metrics_services_manager.h index 4df4d13a47a4..20a4345fe724 100644 --- a/cobalt/browser/metrics/cobalt_metrics_services_manager.h +++ b/cobalt/browser/metrics/cobalt_metrics_services_manager.h @@ -20,8 +20,8 @@ #include "base//memory/scoped_refptr.h" #include "base/single_thread_task_runner.h" +#include "cobalt/base/event_dispatcher.h" #include "cobalt/browser/metrics/cobalt_metrics_services_manager_client.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" #include "components/metrics_services_manager/metrics_services_manager.h" #include "components/metrics_services_manager/metrics_services_manager_client.h" @@ -54,18 +54,9 @@ class CobaltMetricsServicesManager // Destructs the static instance of CobaltMetricsServicesManager. static void DeleteInstance(); - // Sets the upload handler onto the current static instance of - // CobaltMetricsServicesManager. - static void SetOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback); - - // Attempts to clean up the passed reference to CobaltMetricsUploaderCallback, - // IFF it matches the current callback reference in - // CobaltMetricsServiceManager. This is to avoid situations where two clients - // are competing to override the upload handler and prevent one from - // inadvertently clobbering another. - static void RemoveOnUploadHandler( - const CobaltMetricsUploaderCallback* uploader_callback); + // Sets an event dispatcher to call when metric events happen (e.g., on + // upload). + static void SetEventDispatcher(base::EventDispatcher* event_dispatcher); // Toggles whether metric reporting is enabled via // CobaltMetricsServicesManager. @@ -76,11 +67,7 @@ class CobaltMetricsServicesManager static void SetUploadInterval(uint32_t interval_seconds); private: - void SetOnUploadHandlerInternal( - const CobaltMetricsUploaderCallback* uploader_callback); - - void RemoveOnUploadHandlerInternal( - const CobaltMetricsUploaderCallback* uploader_callback); + void SetEventDispatcherInternal(base::EventDispatcher* event_dispatcher); void ToggleMetricsEnabledInternal(bool is_enabled); diff --git a/cobalt/browser/metrics/cobalt_metrics_uploader_callback.h b/cobalt/browser/metrics/cobalt_metrics_uploader_callback.h deleted file mode 100644 index db980c5fa28a..000000000000 --- a/cobalt/browser/metrics/cobalt_metrics_uploader_callback.h +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright 2023 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 COBALT_BROWSER_METRICS_COBALT_METRICS_UPLOADER_CALLBACK_H_ -#define COBALT_BROWSER_METRICS_COBALT_METRICS_UPLOADER_CALLBACK_H_ - -#include - -#include "base/callback.h" -#include "cobalt/h5vcc/h5vcc_metric_type.h" - -namespace cobalt { -namespace browser { -namespace metrics { - -typedef base::RepeatingCallback - CobaltMetricsUploaderCallback; - -} // namespace metrics -} // namespace browser -} // namespace cobalt - -#endif // COBALT_BROWSER_METRICS_COBALT_METRICS_UPLOADER_CALLBACK_H_ diff --git a/cobalt/h5vcc/h5vcc.cc b/cobalt/h5vcc/h5vcc.cc index 8d957b8fb661..5d4d43a879a7 100644 --- a/cobalt/h5vcc/h5vcc.cc +++ b/cobalt/h5vcc/h5vcc.cc @@ -31,7 +31,8 @@ H5vcc::H5vcc(const Settings& settings) { audio_config_array_ = new H5vccAudioConfigArray(); c_val_ = new dom::CValView(); crash_log_ = new H5vccCrashLog(); - metrics_ = new H5vccMetrics(settings.persistent_settings); + metrics_ = + new H5vccMetrics(settings.persistent_settings, settings.event_dispatcher); runtime_ = new H5vccRuntime(settings.event_dispatcher); settings_ = new H5vccSettings(settings.set_web_setting_func, settings.media_module, diff --git a/cobalt/h5vcc/h5vcc_metrics.cc b/cobalt/h5vcc/h5vcc_metrics.cc index ce5e7842172e..583eafddf6b1 100644 --- a/cobalt/h5vcc/h5vcc_metrics.cc +++ b/cobalt/h5vcc/h5vcc_metrics.cc @@ -17,6 +17,9 @@ #include #include "base/values.h" +#include "cobalt/base/event.h" +#include "cobalt/base/event_dispatcher.h" +#include "cobalt/base/on_metric_upload_event.h" #include "cobalt/browser/metrics/cobalt_metrics_service_client.h" #include "cobalt/browser/metrics/cobalt_metrics_services_manager.h" #include "cobalt/h5vcc/h5vcc_metric_type.h" @@ -25,28 +28,34 @@ namespace cobalt { namespace h5vcc { + +H5vccMetrics::H5vccMetrics( + persistent_storage::PersistentSettings* persistent_settings, + base::EventDispatcher* event_dispatcher) + : task_runner_(base::ThreadTaskRunnerHandle::Get()), + persistent_settings_(persistent_settings), + event_dispatcher_(event_dispatcher) { + DCHECK(event_dispatcher_); + on_metric_upload_event_callback_ = + base::Bind(&H5vccMetrics::OnMetricUploadEvent, base::Unretained(this)); + event_dispatcher_->AddEventCallback(base::OnMetricUploadEvent::TypeId(), + on_metric_upload_event_callback_); +} + H5vccMetrics::~H5vccMetrics() { - if (browser::metrics::CobaltMetricsServicesManager::GetInstance() != - nullptr && - run_event_handler_callback_) { - // We need to let the metrics manager know not to call the upload callback - // any longer, otherwise it could crash. - browser::metrics::CobaltMetricsServicesManager::GetInstance() - ->RemoveOnUploadHandler(run_event_handler_callback_.get()); - } + event_dispatcher_->RemoveEventCallback(base::OnMetricUploadEvent::TypeId(), + on_metric_upload_event_callback_); +} + +void H5vccMetrics::OnMetricUploadEvent(const base::Event* event) { + std::unique_ptr on_metric_upload_event( + new base::OnMetricUploadEvent(event)); + RunEventHandler(on_metric_upload_event->metric_type(), + on_metric_upload_event->serialized_proto()); } void H5vccMetrics::OnMetricEvent( const h5vcc::MetricEventHandlerWrapper::ScriptValue& event_handler) { - if (!uploader_callback_) { - run_event_handler_callback_ = std::make_unique< - cobalt::browser::metrics::CobaltMetricsUploaderCallback>( - base::BindRepeating(&H5vccMetrics::RunEventHandler, - base::Unretained(this))); - browser::metrics::CobaltMetricsServicesManager::GetInstance() - ->SetOnUploadHandler(run_event_handler_callback_.get()); - } - uploader_callback_ = new h5vcc::MetricEventHandlerWrapper(this, event_handler); } diff --git a/cobalt/h5vcc/h5vcc_metrics.h b/cobalt/h5vcc/h5vcc_metrics.h index 61e98078265f..93a444aa0711 100644 --- a/cobalt/h5vcc/h5vcc_metrics.h +++ b/cobalt/h5vcc/h5vcc_metrics.h @@ -20,7 +20,8 @@ #include "base/single_thread_task_runner.h" #include "base/threading/thread_task_runner_handle.h" -#include "cobalt/browser/metrics/cobalt_metrics_uploader_callback.h" +#include "cobalt/base/event.h" +#include "cobalt/base/event_dispatcher.h" #include "cobalt/h5vcc/h5vcc_metric_type.h" #include "cobalt/h5vcc/metric_event_handler_wrapper.h" #include "cobalt/persistent_storage/persistent_settings.h" @@ -43,16 +44,15 @@ class H5vccMetrics : public script::Wrappable { typedef MetricEventHandler H5vccMetricEventHandler; explicit H5vccMetrics( - persistent_storage::PersistentSettings* persistent_settings) - : task_runner_(base::ThreadTaskRunnerHandle::Get()), - persistent_settings_(persistent_settings) {} + persistent_storage::PersistentSettings* persistent_settings, + base::EventDispatcher* event_dispatcher); ~H5vccMetrics(); H5vccMetrics(const H5vccMetrics&) = delete; H5vccMetrics& operator=(const H5vccMetrics&) = delete; - // Binds an event handler that will be invoked every time Cobalt wants to + // Binds a JS event handler that will be invoked every time Cobalt wants to // upload a metrics payload. void OnMetricEvent( const MetricEventHandlerWrapper::ScriptValue& event_handler); @@ -83,14 +83,20 @@ class H5vccMetrics : public script::Wrappable { const cobalt::h5vcc::H5vccMetricType& metric_type, const std::string& serialized_proto); - scoped_refptr uploader_callback_; + // Handler method triggered when EventDispatcher sends OnMetricUploadEvents. + void OnMetricUploadEvent(const base::Event* event); - std::unique_ptr - run_event_handler_callback_; + scoped_refptr uploader_callback_; scoped_refptr const task_runner_; persistent_storage::PersistentSettings* persistent_settings_; + + // Non-owned reference used to receive application event callbacks, namely + // metric log upload events. + base::EventDispatcher* event_dispatcher_; + + base::EventCallback on_metric_upload_event_callback_; }; } // namespace h5vcc From 615e305bbb992cf5134edda8c65cbf1f7e32490d Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 13 Oct 2023 12:27:07 -0700 Subject: [PATCH 099/140] Cherry pick PR #1793: Add build_info json to the out directory root. (#1794) Refer to the original PR: https://github.com/youtube/cobalt/pull/1793 b/182513726 Change-Id: Icc182e400611df957d83e2c12df5559bc945c738 Co-authored-by: Brian Ting --- tools/copy_and_filter_out_dir.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/copy_and_filter_out_dir.py b/tools/copy_and_filter_out_dir.py index b9f08ec7464c..9c398cd8766f 100755 --- a/tools/copy_and_filter_out_dir.py +++ b/tools/copy_and_filter_out_dir.py @@ -102,6 +102,12 @@ def CopyAndFilterOutDir(source_out_dir, dest_out_dir): return 0 _IterateAndFilter(source_out_dir, dest_out_dir, copies) + + # Add build_info json to the out directory root. + source_build_info = os.path.join(source_out_dir, 'gen', 'build_info.json') + dest_build_info = os.path.join(dest_out_dir, 'build_info.json') + copies[source_build_info] = dest_build_info + for source, dest in copies.items(): dirname = os.path.dirname(dest) if not os.path.exists(dirname): From 5f83837d211bc64d1fc103002ddd97f992fef476 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 13 Oct 2023 14:14:48 -0700 Subject: [PATCH 100/140] Cherry pick PR #1708: [XB1] Add more cleanup steps to test launcher (#1789) Refer to the original PR: https://github.com/youtube/cobalt/pull/1708 Add more cleanup steps to keep devices healthier over large numbers of automated tests. b/291640038 Change-Id: Iac9e62a4bc40faa8df8441b77a543c04e2f2196d Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/xb1_launcher.py | 33 +++++++++++++++++++------- starboard/xb1/tools/xb1_network_api.py | 11 +++++++-- 2 files changed, 34 insertions(+), 10 deletions(-) diff --git a/starboard/xb1/tools/xb1_launcher.py b/starboard/xb1/tools/xb1_launcher.py index cf252793fa1c..93ab0f1af0fe 100644 --- a/starboard/xb1/tools/xb1_launcher.py +++ b/starboard/xb1/tools/xb1_launcher.py @@ -88,8 +88,16 @@ _ARGS_DIRECTORY = 'content/data/arguments' _STARBOARD_ARGUMENTS_FILE = 'starboard_arguments.txt' _DEFAULT_PACKAGE_NAME = 'GoogleInc.YouTube' +_STUB_PACKAGE_NAME = 'Microsoft.Title.StubApp' +_DEBUG_VC_LIBS_PACKAGE_NAME = 'Microsoft.VCLibs.140.00.Debug' _DEFAULT_APPX_NAME = 'cobalt.appx' _DEFAULT_STAGING_APP_NAME = 'appx' +_EXTENSION_SDK_DIR = os.path.realpath( + os.path.expandvars('%ProgramFiles(x86)%\\Microsoft SDKs' + '\\Windows Kits\\10\\ExtensionSDKs')) +_DEBUG_VC_LIBS_PATH = os.path.join(_EXTENSION_SDK_DIR, 'Microsoft.VCLibs', + '14.0', 'Appx', 'Debug', 'x64', + 'Microsoft.VCLibs.x64.Debug.14.00.appx') _XB1_LOG_FILE_PARAM = 'xb1_log_file' _XB1_PORT = 11443 _XB1_NET_LOG_PORT = 49353 @@ -402,7 +410,9 @@ def UninstallSubPackages(self): for package in packages: try: package_full_name = package['PackageFullName'] - if package_full_name.find(_DEFAULT_PACKAGE_NAME) != -1: + if package_full_name.find( + _DEFAULT_PACKAGE_NAME) != -1 or package_full_name.find( + _STUB_PACKAGE_NAME): if package_full_name not in uninstalled_packages: self._LogLn('Existing YouTube app found on device. Uninstalling: ' + package_full_name) @@ -414,6 +424,9 @@ def UninstallSubPackages(self): except subprocess.CalledProcessError as err: self._LogLn(err.output) + def DeleteLooseApps(self): + self._network_api.ClearLooseAppsFiles() + def Deploy(self): # starboard_arguments.txt is packaged with the appx. It instructs the app # to wait for the NetArgs thread to send command-line args via the socket. @@ -425,33 +438,37 @@ def Deploy(self): raise IOError('Packaged appx not found in package directory. Perhaps ' 'package_cobalt script did not complete successfully.') - existing_package = self.CheckPackageIsDeployed() + existing_package = self.CheckPackageIsDeployed(_DEFAULT_PACKAGE_NAME) if existing_package: self._LogLn('Existing YouTube app found on device. Uninstalling.') self.WinAppDeployCmd('uninstall -package ' + existing_package) + if not self.CheckPackageIsDeployed(_DEBUG_VC_LIBS_PACKAGE_NAME): + self._LogLn('Required dependency missing. Attempting to install.') + self.WinAppDeployCmd(f'install -file "{_DEBUG_VC_LIBS_PATH}"') + self._LogLn('Deleting temporary files') self._network_api.ClearTempFiles() try: - self._LogLn('Installing appx file ' + appx_package_file) - self.WinAppDeployCmd('install -file ' + appx_package_file) + self.WinAppDeployCmd(f'install -file {appx_package_file}') except subprocess.CalledProcessError: # Install exited with non-zero status code, clear everything out, restart, # and attempt another install. self._LogLn('Error installing appx. Attempting a clean install...') self.UninstallSubPackages() + self.DeleteLooseApps() self.RestartDevkit() - self.WinAppDeployCmd('install -file ' + appx_package_file) + self.WinAppDeployCmd(f'install -file {appx_package_file}') # Cleanup starboard arguments file. self.InstallStarboardArgument(None) # Validate that app was installed correctly by checking to make sure # that the full package name can now be found. - def CheckPackageIsDeployed(self): + def CheckPackageIsDeployed(self, package_name): package_list = self.WinAppDeployCmd('list') - package_index = package_list.find(_DEFAULT_PACKAGE_NAME) + package_index = package_list.find(package_name) if package_index == -1: return False return package_list[package_index:].split('\n')[0].strip() @@ -496,7 +513,7 @@ def Run(self): else: self._LogLn('Skipping deploy step.') - if not self.CheckPackageIsDeployed(): + if not self.CheckPackageIsDeployed(_DEFAULT_PACKAGE_NAME): raise IOError('Could not resolve ' + _DEFAULT_PACKAGE_NAME + ' to\n' + 'it\'s full package name after install! This means that' + '\n the package is not deployed correctly!\n\n') diff --git a/starboard/xb1/tools/xb1_network_api.py b/starboard/xb1/tools/xb1_network_api.py index 2d8834e9e363..fac5872c835a 100644 --- a/starboard/xb1/tools/xb1_network_api.py +++ b/starboard/xb1/tools/xb1_network_api.py @@ -48,6 +48,7 @@ _APPX_RELATIVE_PATH = 'appx' _DEVELOPMENT_FILES = 'DevelopmentFiles' +_LOOSE_APPS = 'LooseApps' _LOCAL_APP_DATA = 'LocalAppData' _LOCAL_CACHE_FOLDERNAME = r'\\LocalCache' _TEMP_FILE_FOLDERNAME = r'\\WdpTempWebFolder' @@ -561,17 +562,23 @@ def DeleteFile(self, known_folder_id, path, filename_to_delete): 'path': path }) - def ClearTempFiles(self): + def ClearDevFiles(self, path): file_listing = self._DoJsonRequest( 'GET', _GET_FILES_ENDPOINT, params={ 'knownfolderid': _DEVELOPMENT_FILES, - 'path': _TEMP_FILE_FOLDERNAME + 'path': path }) for file in file_listing['Items']: self.DeleteFile(_DEVELOPMENT_FILES, _TEMP_FILE_FOLDERNAME, file['Name']) + def ClearTempFiles(self): + self.ClearDevFiles(_TEMP_FILE_FOLDERNAME) + + def ClearLooseAppFiles(self): + self.ClearDevFiles(_LOOSE_APPS) + def FindPackage(self, package_name): all_packages = self.GetInstalledPackages() From eee6bd270444c9c088651b4ea1f3bd003df0aa3c Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:10:15 -0700 Subject: [PATCH 101/140] Cherry pick PR #1799: [media] Align AudioDevice output buffer size (#1802) Refer to the original PR: https://github.com/youtube/cobalt/pull/1799 Aligns the size of the AudioDevice output buffer to the fixed size of the written audio data. This ensures that the audio data is not written outside of the buffer's bounds. b/276502329 Co-authored-by: Austin Osagie --- cobalt/audio/audio_device.cc | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/cobalt/audio/audio_device.cc b/cobalt/audio/audio_device.cc index 7aae3b436b67..874d1ba84b0e 100644 --- a/cobalt/audio/audio_device.cc +++ b/cobalt/audio/audio_device.cc @@ -32,6 +32,11 @@ typedef media::AudioBus AudioBus; namespace { const int kRenderBufferSizeFrames = 1024; const int kDefaultFramesPerChannel = 8 * kRenderBufferSizeFrames; + +int AlignUp(int value, int alignment) { + int decremented_value = value - 1; + return decremented_value + alignment - (decremented_value % alignment); +} } // namespace class AudioDevice::Impl { @@ -85,11 +90,13 @@ AudioDevice::Impl::Impl(int number_of_channels, RenderCallback* callback) : number_of_channels_(number_of_channels), output_sample_type_(GetPreferredOutputStarboardSampleType()), render_callback_(callback), - frames_per_channel_(std::max(SbAudioSinkGetMinBufferSizeInFrames( - number_of_channels, output_sample_type_, - kStandardOutputSampleRate) + - kRenderBufferSizeFrames * 2, - kDefaultFramesPerChannel)), + frames_per_channel_( + std::max(AlignUp(SbAudioSinkGetMinBufferSizeInFrames( + number_of_channels, output_sample_type_, + kStandardOutputSampleRate) + + kRenderBufferSizeFrames * 2, + kRenderBufferSizeFrames), + kDefaultFramesPerChannel)), input_audio_bus_(static_cast(number_of_channels), static_cast(kRenderBufferSizeFrames), GetPreferredOutputSampleType(), AudioBus::kPlanar), @@ -99,6 +106,7 @@ AudioDevice::Impl::Impl(int number_of_channels, RenderCallback* callback) DCHECK(number_of_channels_ == 1 || number_of_channels_ == 2) << "Invalid number of channels: " << number_of_channels_; DCHECK(render_callback_); + DCHECK(frames_per_channel_ % kRenderBufferSizeFrames == 0); DCHECK(SbAudioSinkIsAudioFrameStorageTypeSupported( kSbMediaAudioFrameStorageTypeInterleaved)) << "Only interleaved frame storage is supported."; From 8d09f455ffab51b54165dad4763f1297b7392abd Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 13 Oct 2023 16:10:35 -0700 Subject: [PATCH 102/140] Cherry pick PR #1762: [nplb] Discard infinite audio durations in tests (#1801) Refer to the original PR: https://github.com/youtube/cobalt/pull/1762 Changes SbPlayerWriteSampleTest.PartialAudioDiscardAll to write buffers with an infinite duration discarded from the front or back. b/302020010 Co-authored-by: Austin Osagie --- starboard/nplb/player_write_sample_test.cc | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/starboard/nplb/player_write_sample_test.cc b/starboard/nplb/player_write_sample_test.cc index 414a4eabe400..5bc16531cc8d 100644 --- a/starboard/nplb/player_write_sample_test.cc +++ b/starboard/nplb/player_write_sample_test.cc @@ -263,11 +263,15 @@ TEST_P(SbPlayerWriteSampleTest, PartialAudioDiscardAll) { SbTime current_time_offset = 0; int num_of_buffers_per_write = player_fixture.ConvertDurationToAudioBufferCount(kDurationPerWrite); + int count = 0; while (current_time_offset < kDurationToPlay) { + const SbTime kDurationToDiscard = + count % 2 == 0 ? kSbTimeSecond : kSbTimeMax; + count++; // Discard from front. for (int i = 0; i < kNumberOfBuffersToDiscard; i++) { samples.AddAudioSamples(written_buffer_index, 1, current_time_offset, - kSbTimeSecond, 0); + kDurationToDiscard, 0); } samples.AddAudioSamples(written_buffer_index, num_of_buffers_per_write); @@ -277,7 +281,7 @@ TEST_P(SbPlayerWriteSampleTest, PartialAudioDiscardAll) { // Discard from back. for (int i = 0; i < kNumberOfBuffersToDiscard; i++) { samples.AddAudioSamples(written_buffer_index, 1, current_time_offset, 0, - kSbTimeSecond); + kDurationToDiscard); } } samples.AddAudioEOS(); From f498ddde94d7d7204fa16ff606ec311bb4eb2230 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 13 Oct 2023 20:14:09 -0700 Subject: [PATCH 103/140] Cherry pick PR #1784: [nplb] Add benchmark log suppression switch (#1804) --- starboard/nplb/performance_helpers.h | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/starboard/nplb/performance_helpers.h b/starboard/nplb/performance_helpers.h index 62639df01e98..7a9b538ddc50 100644 --- a/starboard/nplb/performance_helpers.h +++ b/starboard/nplb/performance_helpers.h @@ -38,11 +38,16 @@ void TestPerformanceOfFunction(const char* const name_of_f, // Measure time pre calls to |f|. const SbTimeMonotonic time_start = SbTimeGetMonotonicNow(); + SbLogPriority initial_log_level = starboard::logging::GetMinLogLevel(); + starboard::logging::SetMinLogLevel(kSbLogPriorityFatal); + // Call |f| |count_calls| times. for (int i = 0; i < count_calls; ++i) { f(args...); } + starboard::logging::SetMinLogLevel(initial_log_level); + // Measure time post calls to |f|. const SbTimeMonotonic time_last = SbTimeGetMonotonicNow(); const SbTimeMonotonic time_delta = time_last - time_start; From 544e724f007ce2c436848b1560406af93cf266f6 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:51:59 -0700 Subject: [PATCH 104/140] Cherry pick PR #1681: Add 'memoryLimit' attribute to SourceBuffer. (#1788) Refer to the original PR: https://github.com/youtube/cobalt/pull/1681 This allows a web app to override the default memory_limit on a SourceBufferStream which backs an instance of source_buffer. A SourceBufferStream is not created until after the first Init Segment is appended, so if you try to read the memory_limit before then, the web app will get an exception. However you can still pre-emptively set the memory_limit which will be applied to SourceBufferStream upon creation. b/291291120 Change-Id: Id577df24fadad4bb6405a1ecf1439942cd988837 Co-authored-by: thorsten sideb0ard --- .../configure-source-buffer-memory.html | 20 +++ .../configure-source-buffer-memory.js | 142 ++++++++++++++++++ .../vp9-720p.webm | Bin 0 -> 371974 bytes cobalt/dom/source_buffer.cc | 33 ++++ cobalt/dom/source_buffer.h | 3 + cobalt/dom/source_buffer.idl | 6 + .../chromium/media/filters/chunk_demuxer.cc | 29 ++++ .../chromium/media/filters/chunk_demuxer.h | 5 + .../media/filters/source_buffer_state.cc | 34 ++++- .../media/filters/source_buffer_state.h | 5 + .../media/filters/source_buffer_stream.cc | 13 +- .../media/filters/source_buffer_stream.h | 11 ++ 12 files changed, 295 insertions(+), 6 deletions(-) create mode 100644 cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.html create mode 100644 cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.js create mode 100644 cobalt/demos/content/configure-source-buffer-memory/vp9-720p.webm diff --git a/cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.html b/cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.html new file mode 100644 index 000000000000..3c40603694d7 --- /dev/null +++ b/cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.html @@ -0,0 +1,20 @@ + + + + Configure Source Buffer Memory + + + + +
+
+ + diff --git a/cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.js b/cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.js new file mode 100644 index 000000000000..3f2c6756b480 --- /dev/null +++ b/cobalt/demos/content/configure-source-buffer-memory/configure-source-buffer-memory.js @@ -0,0 +1,142 @@ +// Copyright 2023 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. + +"use strict"; + +const QUOTA_EXCEEDED_ERROR_CODE = 22; +const mimeCodec = 'video/webm; codecs="vp9"'; +const assetURL = 'vp9-720p.webm'; + +const assetDuration = 15; +const assetSize = 344064; + +let status_div; +let video; + + +function fetchAB(url, cb) { + console.log("Fetching.. ", url); + const xhr = new XMLHttpRequest(); + xhr.open("get", url); + xhr.responseType = "arraybuffer"; + xhr.onload = () => { + console.log("onLoad - calling Callback"); + cb(xhr.response); + }; + console.log('Sending request for media segment ...'); + xhr.send(); +} + +function testAppendToBuffer(media_source, mem_limit) { + const mediaSource = media_source; + const sourceBuffer = mediaSource.addSourceBuffer(mimeCodec); + if (mem_limit > 0) { + status_div.innerHTML += "Test SourceBuffer, setting memoryLimit to " + mem_limit + "
"; + sourceBuffer.memoryLimit = mem_limit; + } else { + status_div.innerHTML += "Test SourceBuffer, leaving memoryLimit at default
"; + } + + let MIN_SIZE = 12 * 1024 * 1024; + let ESTIMATED_MIN_TIME = 12; + + fetchAB(assetURL, (buf) => { + let expectedTime = 0; + let expectedSize = 0; + let appendCount = 0; + + + let onBufferFull = function(buffer_was_full) { + console.log("OnBufferFull! Quota exceeded? " + buffer_was_full + " appendCount:" + appendCount + " expectedTime:" + expectedTime); + status_div.innerHTML += "Finished! Quota exceeded? " + buffer_was_full + " appendCount:" + appendCount + " appended " + appendCount * assetSize + "
"; + } + + sourceBuffer.addEventListener("updateend", function onupdateend() { + console.log("Update end. State is " + sourceBuffer.updating); + appendCount++; + console.log("Append Count" + appendCount); + if (sourceBuffer.buffered.start(0) > 0 || expectedTime > sourceBuffer.buffered.end(0)) { + sourceBuffer.removeEventListener('updatedend', onupdateend); + onBufferFull(false); + } else { + expectedTime += assetDuration; + expectedSize += assetSize; + if (expectedSize > (10 * MIN_SIZE)) { + sourceBuffer.removeEventListener('updateend', onupdateend); + onBufferFull(false); + return; + } + + try { + sourceBuffer.timestampOffset = expectedTime; + } catch(e) { + console.log("Unexpected error: " + e); + } + + try { + sourceBuffer.appendBuffer(buf); + } catch(e) { + console.log("Wuff! QUOTA_EXCEEDED_ERROR!"); + status_div.innerHTML += "Wuff! QUOTA_EXCEEDED
"; + if (e.code == QUOTA_EXCEEDED_ERROR_CODE) { + sourceBuffer.removeEventListener('updateend', onupdateend); + onBufferFull(true); + } else { + console.log("Unexpected error: " + e); + } + } + } + }); + + console.log("First Append!"); + sourceBuffer.appendBuffer(buf); + status_div.innerHTML += "First append. MemoryLimit is:" + sourceBuffer.memoryLimit + ".
"; + }); +} + +function onSourceOpen() { + console.log("Source Open. This state:", this.readyState); // open + status_div.innerHTML += "Source Open. This state:" + this.readyState + "
"; + status_div.innerHTML += "Lets test first source_buffer, defaults..
"; + testAppendToBuffer(this, 0); + + let new_mem_limit = 400 * 1024 * 1024; + status_div.innerHTML += "

Lets test second source_buffer, setting memory to:" + new_mem_limit + "
"; + testAppendToBuffer(this, new_mem_limit); + video.play(); +} + + +function createMediaSource() { + + console.log('Video Get Element By Id...'); + video = document.getElementById('video'); + status_div = document.getElementById('status'); + status_div.innerHTML += 'Video Get Element By Id...
'; + + console.log('Create Media Source...'); + status_div.innerHTML += 'Create Media Source...
'; + var mediaSource = new MediaSource; + + console.log('Attaching MediaSource to video element ...'); + status_div.innerHTML += 'Attaching MediaSource to video element ...
'; + video.src = window.URL.createObjectURL(mediaSource); + + console.log('Add event listener..'); + status_div.innerHTML += 'Add event listener..
'; + mediaSource.addEventListener('sourceopen', onSourceOpen); + +} + +addEventListener('load', createMediaSource); diff --git a/cobalt/demos/content/configure-source-buffer-memory/vp9-720p.webm b/cobalt/demos/content/configure-source-buffer-memory/vp9-720p.webm new file mode 100644 index 0000000000000000000000000000000000000000..08a670e72367681ce030c7982b2e5f63f06464fa GIT binary patch literal 371974 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%0AC{MowKyQ)*(o-RHD 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 z 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

ws@#C!k*Nc60j%H7Qf^hk`==DT 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

7ymryMt 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 literal 0 HcmV?d00001 diff --git a/cobalt/dom/source_buffer.cc b/cobalt/dom/source_buffer.cc index 039303fe8a96..1ad5a59630de 100644 --- a/cobalt/dom/source_buffer.cc +++ b/cobalt/dom/source_buffer.cc @@ -521,6 +521,39 @@ void SourceBuffer::TraceMembers(script::Tracer* tracer) { tracer->Trace(video_tracks_); } +size_t SourceBuffer::memory_limit( + script::ExceptionState* exception_state) const { + if (!chunk_demuxer_) { + web::DOMException::Raise(web::DOMException::kInvalidStateErr, + exception_state); + return 0; + } + + size_t memory_limit = chunk_demuxer_->GetSourceBufferStreamMemoryLimit(id_); + + if (memory_limit == 0) { + web::DOMException::Raise(web::DOMException::kInvalidStateErr, + exception_state); + } + return memory_limit; +} + +void SourceBuffer::set_memory_limit(size_t memory_limit, + script::ExceptionState* exception_state) { + if (!chunk_demuxer_) { + web::DOMException::Raise(web::DOMException::kInvalidStateErr, + exception_state); + return; + } + + if (memory_limit == 0) { + web::DOMException::Raise(web::DOMException::kNotSupportedErr, + exception_state); + return; + } + chunk_demuxer_->SetSourceBufferStreamMemoryLimit(id_, memory_limit); +} + void SourceBuffer::OnInitSegmentReceived(std::unique_ptr tracks) { if (!first_initialization_segment_received_) { media_source_->SetSourceBufferActive(this, true); diff --git a/cobalt/dom/source_buffer.h b/cobalt/dom/source_buffer.h index 5869760c489a..0da8e81ae90f 100644 --- a/cobalt/dom/source_buffer.h +++ b/cobalt/dom/source_buffer.h @@ -144,6 +144,9 @@ class SourceBuffer : public web::EventTarget { DEFINE_WRAPPABLE_TYPE(SourceBuffer); void TraceMembers(script::Tracer* tracer) override; + size_t memory_limit(script::ExceptionState* exception_state) const; + void set_memory_limit(size_t limit, script::ExceptionState* exception_state); + private: typedef ::media::MediaTracks MediaTracks; typedef script::ArrayBuffer ArrayBuffer; diff --git a/cobalt/dom/source_buffer.idl b/cobalt/dom/source_buffer.idl index 0d81b03db802..a963802b1ea5 100644 --- a/cobalt/dom/source_buffer.idl +++ b/cobalt/dom/source_buffer.idl @@ -39,4 +39,10 @@ interface SourceBuffer : EventTarget { // `InvalidStateError` if the SourceBuffer object has been removed from the // MediaSource object. [RaisesException] readonly attribute double writeHead; + + // Non standard stream memory limit modifier. This will override the default + // stream memory limit which is tied to the resolution of the video. + // This will be passed down to the SourceBufferStream associated with this + // instance. + [RaisesException] attribute unsigned long long memoryLimit; }; diff --git a/third_party/chromium/media/filters/chunk_demuxer.cc b/third_party/chromium/media/filters/chunk_demuxer.cc index 2b729a451ec3..e24189d7fb41 100644 --- a/third_party/chromium/media/filters/chunk_demuxer.cc +++ b/third_party/chromium/media/filters/chunk_demuxer.cc @@ -235,6 +235,19 @@ base::TimeDelta ChunkDemuxerStream::GetWriteHead() const { return write_head_; } +size_t ChunkDemuxerStream::GetStreamMemoryLimit() { + DCHECK(stream_); + base::AutoLock auto_lock(lock_); + return stream_->memory_limit(); +} + +void ChunkDemuxerStream::SetStreamMemoryLimitOverride(size_t memory_limit) { + DCHECK(stream_); + base::AutoLock auto_lock(lock_); + stream_->set_memory_limit_override(memory_limit); +} + + #endif // defined(STARBOARD) void ChunkDemuxerStream::OnMemoryPressure( @@ -1110,6 +1123,22 @@ base::TimeDelta ChunkDemuxer::GetWriteHead(const std::string& id) const { return iter->second[0]->GetWriteHead(); } +void ChunkDemuxer::SetSourceBufferStreamMemoryLimit(const std::string& id, + size_t limit) { + base::AutoLock auto_lock(lock_); + DCHECK(source_state_map_.find(id) != source_state_map_.end()); + source_state_map_[id]->SetSourceBufferStreamMemoryLimit(limit); +} + +size_t ChunkDemuxer::GetSourceBufferStreamMemoryLimit(const std::string& id) { + + base::AutoLock auto_lock(lock_); + if (source_state_map_.find(id) == source_state_map_.end()) { + return 0; + } + return source_state_map_[id]->GetSourceBufferStreamMemoryLimit(); +} + #endif // defined(STARBOARD) bool ChunkDemuxer::AppendData(const std::string& id, diff --git a/third_party/chromium/media/filters/chunk_demuxer.h b/third_party/chromium/media/filters/chunk_demuxer.h index 2e7129194e2e..76a01f414f41 100644 --- a/third_party/chromium/media/filters/chunk_demuxer.h +++ b/third_party/chromium/media/filters/chunk_demuxer.h @@ -132,6 +132,8 @@ class MEDIA_EXPORT ChunkDemuxerStream : public DemuxerStream { // DemuxerStream methods. #if defined(STARBOARD) std::string mime_type() const override { return mime_type_; } + size_t GetStreamMemoryLimit(); + void SetStreamMemoryLimitOverride(size_t memory_limit); #endif // defined (STARBOARD) #if defined(STARBOARD) @@ -392,6 +394,9 @@ class MEDIA_EXPORT ChunkDemuxer : public Demuxer { #if defined(STARBOARD) base::TimeDelta GetWriteHead(const std::string& id) const; + + void SetSourceBufferStreamMemoryLimit(const std::string& guid, size_t limit); + size_t GetSourceBufferStreamMemoryLimit(const std::string& guid); #endif // defined(STARBOARD) void OnMemoryPressure( diff --git a/third_party/chromium/media/filters/source_buffer_state.cc b/third_party/chromium/media/filters/source_buffer_state.cc index 8951bc8f3e13..955dcf843e71 100644 --- a/third_party/chromium/media/filters/source_buffer_state.cc +++ b/third_party/chromium/media/filters/source_buffer_state.cc @@ -915,9 +915,41 @@ bool SourceBufferState::OnNewConfigs( return success; } +#if defined(STARBOARD) +void SourceBufferState::SetSourceBufferStreamMemoryLimit(size_t limit) { + LOG(INFO) << "Setting SourceBuffferStream MemoryLimit Override: " << limit; + stream_memory_limit_override_ = limit; + SetStreamMemoryLimits(); +} + +size_t SourceBufferState::GetSourceBufferStreamMemoryLimit() { + // a source buffer can be backed my multiple Demuxer streams, although at + // YouTube we usually only have a single one. If we've set an override then + // all values will be the same, but we shouldn't assume that, so instead we'll + // return the largest here if there are multiple. + size_t memory_limit = 0; + for (const auto& it : audio_streams_) { + memory_limit = std::max(memory_limit, it.second->GetStreamMemoryLimit()); + } + for (const auto& it : video_streams_) { + memory_limit = std::max(memory_limit, it.second->GetStreamMemoryLimit()); + } + return memory_limit; +} +#endif // defined(STARBOARD) + void SourceBufferState::SetStreamMemoryLimits() { #if defined(STARBOARD) - // Cobalt doesn't get stream memory limits from the command line. + LOG(INFO) << "Custom SourceBuffer memory limit=" + << stream_memory_limit_override_; + if (stream_memory_limit_override_) { + for (const auto& it : audio_streams_) { + it.second->SetStreamMemoryLimitOverride(stream_memory_limit_override_); + } + for (const auto& it : video_streams_) { + it.second->SetStreamMemoryLimitOverride(stream_memory_limit_override_); + } + } #else // defined(STARBOARD) size_t audio_buf_size_limit = GetMSEBufferSizeLimitIfExists(switches::kMSEAudioBufferSizeLimitMb); diff --git a/third_party/chromium/media/filters/source_buffer_state.h b/third_party/chromium/media/filters/source_buffer_state.h index 31f5bab8bc12..62ee4f7fd16c 100644 --- a/third_party/chromium/media/filters/source_buffer_state.h +++ b/third_party/chromium/media/filters/source_buffer_state.h @@ -159,6 +159,11 @@ class MEDIA_EXPORT SourceBufferState { void SetParseWarningCallback(SourceBufferParseWarningCB parse_warning_cb); +#if defined(STARBOARD) + void SetSourceBufferStreamMemoryLimit(size_t limit); + size_t GetSourceBufferStreamMemoryLimit(); + size_t stream_memory_limit_override_ = 0; +#endif // defined(STARBOARD) private: // State advances through this list to PARSER_INITIALIZED. // The intent is to ensure at least one config is received prior to parser diff --git a/third_party/chromium/media/filters/source_buffer_stream.cc b/third_party/chromium/media/filters/source_buffer_stream.cc index a1e9e2e6bc6e..6d6789f0f3c9 100644 --- a/third_party/chromium/media/filters/source_buffer_stream.cc +++ b/third_party/chromium/media/filters/source_buffer_stream.cc @@ -1900,11 +1900,14 @@ bool SourceBufferStream::UpdateVideoConfig(const VideoDecoderConfig& config, video_configs_[append_config_index_] = config; #if defined(STARBOARD) - // Dynamically increase |memory_limit_| when video resolution goes up. - memory_limit_ = std::max( - memory_limit_, - GetDemuxerStreamVideoMemoryLimit(Demuxer::DemuxerTypes::kChunkDemuxer, - &config, mime_type_)); + // Dynamically increase |memory_limit_| when video resolution goes up as long + // as we haven't set a manual override. + if (!memory_override_) { + memory_limit_ = std::max( + memory_limit_, + GetDemuxerStreamVideoMemoryLimit(Demuxer::DemuxerTypes::kChunkDemuxer, + &config, mime_type_)); + } #endif // defined(STARBOARD) return true; } diff --git a/third_party/chromium/media/filters/source_buffer_stream.h b/third_party/chromium/media/filters/source_buffer_stream.h index 356db450ce3f..248b38ed40e9 100644 --- a/third_party/chromium/media/filters/source_buffer_stream.h +++ b/third_party/chromium/media/filters/source_buffer_stream.h @@ -188,6 +188,16 @@ class MEDIA_EXPORT SourceBufferStream { memory_limit_ = memory_limit; } +#if defined(STARBOARD) + size_t memory_limit() const { + return memory_limit_; + } + void set_memory_limit_override(size_t memory_limit) { + memory_limit_ = memory_limit; + memory_override_ = true; + } +#endif // defined (STARBOARD) + private: friend class SourceBufferStreamTest; @@ -405,6 +415,7 @@ class MEDIA_EXPORT SourceBufferStream { base::TimeDelta GetBufferedDurationForGarbageCollection() const; const std::string mime_type_; + bool memory_override_ = false; #endif // defined (STARBOARD) // Used to report log messages that can help the web developer figure out what From f58b9d859bd24329679a4eb2936f2fd95009283f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 16 Oct 2023 13:54:02 -0700 Subject: [PATCH 105/140] Cherry pick PR #1787: Fixing histogram bug, off by one error on check (#1790) Refer to the original PR: https://github.com/youtube/cobalt/pull/1787 We ran into an off-by-1 error with Telemetry as we were using UmaHistogramEnumeration. The histograms MACROs checks that sample is less than max - https://source.corp.google.com/h/lbshell-internal/cobalt_src/+/COBALT:base/metrics/histogram_functions.h;l=53 Chrome actually uses UmaHistogramExactLinear so they can add a +1 to the PIPELINE_STATUS_MAX https://source.chromium.org/chromium/chromium/src/+/main:media/mojo/services/media_metrics_provider.cc;l=144 This changes puts us more in line with the Chrome implementation. b/287670693 Co-authored-by: thorsten sideb0ard --- cobalt/media/base/metrics_provider.cc | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cobalt/media/base/metrics_provider.cc b/cobalt/media/base/metrics_provider.cc index de9d41e39e95..510d5a867feb 100644 --- a/cobalt/media/base/metrics_provider.cc +++ b/cobalt/media/base/metrics_provider.cc @@ -73,24 +73,24 @@ void MediaMetricsProvider::SetIsEME() { void MediaMetricsProvider::ReportPipelineUMA() { ScopedLock scoped_lock(mutex_); if (uma_info_.has_video && uma_info_.has_audio) { - base::UmaHistogramEnumeration(GetUMANameForAVStream(uma_info_), + base::UmaHistogramExactLinear(GetUMANameForAVStream(uma_info_), uma_info_.last_pipeline_status, - PipelineStatus::PIPELINE_STATUS_MAX); + PipelineStatus::PIPELINE_STATUS_MAX + 1); } else if (uma_info_.has_audio) { - base::UmaHistogramEnumeration("Cobalt.Media.PipelineStatus.AudioOnly", + base::UmaHistogramExactLinear("Cobalt.Media.PipelineStatus.AudioOnly", uma_info_.last_pipeline_status, - PipelineStatus::PIPELINE_STATUS_MAX); + PipelineStatus::PIPELINE_STATUS_MAX + 1); } else if (uma_info_.has_video) { - base::UmaHistogramEnumeration("Cobalt.Media.PipelineStatus.VideoOnly", + base::UmaHistogramExactLinear("Cobalt.Media.PipelineStatus.VideoOnly", uma_info_.last_pipeline_status, - PipelineStatus::PIPELINE_STATUS_MAX); + PipelineStatus::PIPELINE_STATUS_MAX + 1); } else { // Note: This metric can be recorded as a result of normal operation with // Media Source Extensions. If a site creates a MediaSource object but never // creates a source buffer or appends data, PIPELINE_OK will be recorded. - base::UmaHistogramEnumeration("Cobalt.Media.PipelineStatus.Unsupported", + base::UmaHistogramExactLinear("Cobalt.Media.PipelineStatus.Unsupported", uma_info_.last_pipeline_status, - PipelineStatus::PIPELINE_STATUS_MAX); + PipelineStatus::PIPELINE_STATUS_MAX + 1); } // Report whether this player ever saw a playback event. Used to measure the From 7cc3940f48b978edd0e37336cd4074e7e5990c76 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 16 Oct 2023 14:43:02 -0700 Subject: [PATCH 106/140] Cherry pick PR #1800: [android] Avoid exceptions in MediaCodecBridge. (#1805) Refer to the original PR: https://github.com/youtube/cobalt/pull/1800 This avoids exceptions being thrown from MediaCodecBridge.textureWidth() and MediaCodecBridge.textureHeight(). b/298692099 Co-authored-by: Jelle Foks --- .../src/main/java/dev/cobalt/media/MediaCodecBridge.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java index 6d1c643d2e32..8164adda1569 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecBridge.java @@ -299,13 +299,17 @@ private int status() { @SuppressWarnings("unused") @UsedByNative private int textureWidth() { - return mFormat.getInteger(MediaFormat.KEY_WIDTH); + return (mFormat != null && mFormat.containsKey(MediaFormat.KEY_WIDTH)) + ? mFormat.getInteger(MediaFormat.KEY_WIDTH) + : 0; } @SuppressWarnings("unused") @UsedByNative private int textureHeight() { - return mFormat.getInteger(MediaFormat.KEY_HEIGHT); + return (mFormat != null && mFormat.containsKey(MediaFormat.KEY_HEIGHT)) + ? mFormat.getInteger(MediaFormat.KEY_HEIGHT) + : 0; } @SuppressWarnings("unused") From f9c83cf3560d0396567890684c84ad1617228d54 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 16 Oct 2023 20:13:52 -0700 Subject: [PATCH 107/140] Cherry pick PR #1808: Properly set foreground service permissions (#1809) Refer to the original PR: https://github.com/youtube/cobalt/pull/1808 Per https://developer.android.com/about/versions/14/changes/fgs-types-required#media we should be setting that we are using the FOREGROUND_SERVICE_MEDIA_PLAYBACK permission and also passing the corresponding type constants when we start the foreground service. b/305802411 Co-authored-by: Garo Bournoutian --- starboard/android/apk/app/src/app/AndroidManifest.xml | 1 + .../main/java/dev/cobalt/coat/MediaPlaybackService.java | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/starboard/android/apk/app/src/app/AndroidManifest.xml b/starboard/android/apk/app/src/app/AndroidManifest.xml index 6b2602d5133c..0c23b475dceb 100644 --- a/starboard/android/apk/app/src/app/AndroidManifest.xml +++ b/starboard/android/apk/app/src/app/AndroidManifest.xml @@ -31,6 +31,7 @@ + diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java index 8bfb464b1d86..8f7742bf9f99 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/MediaPlaybackService.java @@ -22,6 +22,7 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.pm.ServiceInfo; import android.os.Build.VERSION; import android.os.IBinder; import android.os.RemoteException; @@ -82,7 +83,12 @@ public void onDestroy() { public void startService() { if (this.channelCreated) { try { - startForeground(NOTIFICATION_ID, buildNotification()); + if (VERSION.SDK_INT >= 29) { + startForeground( + NOTIFICATION_ID, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST); + } else { + startForeground(NOTIFICATION_ID, buildNotification()); + } } catch (IllegalStateException e) { Log.e(TAG, "Failed to start Foreground Service", e); } From e3ba3a348a1f86c49917fdb2490d556ecb71c361 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 17 Oct 2023 10:30:10 -0700 Subject: [PATCH 108/140] Cherry pick PR #1724: Ensure cache quotas are read and written from same thread. (#1807) Refer to the original PR: https://github.com/youtube/cobalt/pull/1724 b/300175596 Co-authored-by: aee <117306596+aee-google@users.noreply.github.com> --- cobalt/browser/application.cc | 3 - cobalt/cache/cache.cc | 37 +--- cobalt/cache/cache.h | 12 +- cobalt/h5vcc/h5vcc_storage.cc | 41 ++-- cobalt/network/url_request_context.cc | 48 ++--- .../persistent_storage/persistent_settings.cc | 4 +- net/BUILD.gn | 1 + net/disk_cache/cobalt/cobalt_backend_impl.cc | 75 +++++--- net/disk_cache/cobalt/cobalt_backend_impl.h | 4 +- net/disk_cache/cobalt/resource_type.cc | 175 ++++++++++++++++++ net/disk_cache/cobalt/resource_type.h | 30 +-- net/disk_cache/disk_cache.cc | 6 +- 12 files changed, 304 insertions(+), 132 deletions(-) create mode 100644 net/disk_cache/cobalt/resource_type.cc diff --git a/cobalt/browser/application.cc b/cobalt/browser/application.cc index 117452f99778..ebf95f7ba587 100644 --- a/cobalt/browser/application.cc +++ b/cobalt/browser/application.cc @@ -703,9 +703,6 @@ Application::Application(const base::Closure& quit_closure, bool should_preload, base::kApplicationStateStarted, kWatchdogTimeInterval, kWatchdogTimeWait, watchdog::NONE); - cobalt::cache::Cache::GetInstance()->set_persistent_settings( - persistent_settings_.get()); - base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); base::Optional requested_viewport_size = GetRequestedViewportSize(command_line); diff --git a/cobalt/cache/cache.cc b/cobalt/cache/cache.cc index d88a6ec533f8..42eee8ebde51 100644 --- a/cobalt/cache/cache.cc +++ b/cobalt/cache/cache.cc @@ -25,7 +25,6 @@ #include "base/strings/string_number_conversions.h" #include "base/values.h" #include "cobalt/configuration/configuration.h" -#include "cobalt/persistent_storage/persistent_settings.h" #include "net/disk_cache/cobalt/cobalt_backend_impl.h" #include "starboard/configuration_constants.h" #include "starboard/extension/javascript_cache.h" @@ -201,19 +200,6 @@ std::unique_ptr> Cache::Retrieve( return nullptr; } -void Cache::set_enabled(bool enabled) { enabled_ = enabled; } - -void Cache::set_persistent_settings( - persistent_storage::PersistentSettings* persistent_settings) { - persistent_settings_ = persistent_settings; - - // Guaranteed to be called before any calls to Retrieve() - // since set_persistent_settings() is called from the Application() - // constructor before the NetworkModule is initialized. - set_enabled(persistent_settings_->GetPersistentSettingAsBool( - disk_cache::kCacheEnabledPersistentSettingsKey, true)); -} - MemoryCappedDirectory* Cache::GetMemoryCappedDirectory( disk_cache::ResourceType resource_type) { base::AutoLock auto_lock(lock_); @@ -222,16 +208,6 @@ MemoryCappedDirectory* Cache::GetMemoryCappedDirectory( return it->second.get(); } - // Read in size from persistent storage. - auto metadata = disk_cache::kTypeMetadata[resource_type]; - if (persistent_settings_) { - uint32_t bucket_size = static_cast( - persistent_settings_->GetPersistentSettingAsDouble( - metadata.directory, metadata.max_size_bytes)); - disk_cache::kTypeMetadata[resource_type] = {metadata.directory, - bucket_size}; - } - auto cache_directory = GetCacheDirectory(resource_type); auto max_size = GetMaxCacheStorageInBytes(resource_type); if (!cache_directory || !max_size) { @@ -248,16 +224,9 @@ MemoryCappedDirectory* Cache::GetMemoryCappedDirectory( void Cache::Resize(disk_cache::ResourceType resource_type, uint32_t bytes) { if (resource_type != disk_cache::ResourceType::kCacheApi && resource_type != disk_cache::ResourceType::kCompiledScript && - resource_type != disk_cache::ResourceType::kServiceWorkerScript) + resource_type != disk_cache::ResourceType::kServiceWorkerScript) { return; - if (bytes == disk_cache::kTypeMetadata[resource_type].max_size_bytes) return; - - if (persistent_settings_) { - persistent_settings_->SetPersistentSetting( - disk_cache::kTypeMetadata[resource_type].directory, - std::make_unique(static_cast(bytes))); } - disk_cache::kTypeMetadata[resource_type].max_size_bytes = bytes; auto* memory_capped_directory = GetMemoryCappedDirectory(resource_type); if (memory_capped_directory) { memory_capped_directory->Resize(bytes); @@ -273,7 +242,7 @@ base::Optional Cache::GetMaxCacheStorageInBytes( case disk_cache::ResourceType::kCacheApi: case disk_cache::ResourceType::kCompiledScript: case disk_cache::ResourceType::kServiceWorkerScript: - return disk_cache::kTypeMetadata[resource_type].max_size_bytes; + return disk_cache::settings::GetQuota(resource_type); default: return base::nullopt; } @@ -334,7 +303,7 @@ bool Cache::CanCache(disk_cache::ResourceType resource_type, resource_type == disk_cache::ResourceType::kCacheApi) { return true; } - if (!enabled_) { + if (!disk_cache::settings::GetCacheEnabled()) { return false; } if (resource_type == disk_cache::ResourceType::kCompiledScript) { diff --git a/cobalt/cache/cache.h b/cobalt/cache/cache.h index 40bd22561b9e..e98f3355dc95 100644 --- a/cobalt/cache/cache.h +++ b/cobalt/cache/cache.h @@ -26,12 +26,12 @@ #include "base/files/file_enumerator.h" #include "base/files/file_path.h" #include "base/macros.h" +#include "base/message_loop/message_loop.h" #include "base/optional.h" #include "base/synchronization/lock.h" #include "base/synchronization/waitable_event.h" #include "base/values.h" #include "cobalt/cache/memory_capped_directory.h" -#include "cobalt/persistent_storage/persistent_settings.h" #include "net/disk_cache/cobalt/resource_type.h" namespace base { @@ -67,14 +67,9 @@ class Cache { base::Optional GetMaxCacheStorageInBytes( disk_cache::ResourceType resource_type); - void set_enabled(bool enabled); - - void set_persistent_settings( - persistent_storage::PersistentSettings* persistent_settings); - private: friend struct base::DefaultSingletonTraits; - Cache() {} + Cache() = default; MemoryCappedDirectory* GetMemoryCappedDirectory( disk_cache::ResourceType resource_type); @@ -91,9 +86,6 @@ class Cache { std::map>> pending_; - bool enabled_ = true; - - persistent_storage::PersistentSettings* persistent_settings_ = nullptr; DISALLOW_COPY_AND_ASSIGN(Cache); }; // class Cache diff --git a/cobalt/h5vcc/h5vcc_storage.cc b/cobalt/h5vcc/h5vcc_storage.cc index ebbd3060ac0d..346c1040ffdb 100644 --- a/cobalt/h5vcc/h5vcc_storage.cc +++ b/cobalt/h5vcc/h5vcc_storage.cc @@ -74,13 +74,12 @@ void ClearDirectory(const base::FilePath& file_path) { } void DeleteCacheResourceTypeDirectory(disk_cache::ResourceType type) { - auto metadata = disk_cache::kTypeMetadata[type]; + std::string directory = disk_cache::defaults::GetSubdirectory(type); std::vector cache_dir(kSbFileMaxPath + 1, 0); SbSystemGetPath(kSbSystemPathCacheDirectory, cache_dir.data(), kSbFileMaxPath); base::FilePath cache_type_dir = - base::FilePath(cache_dir.data()) - .Append(FILE_PATH_LITERAL(metadata.directory)); + base::FilePath(cache_dir.data()).Append(FILE_PATH_LITERAL(directory)); ClearDirectory(cache_type_dir); } @@ -313,11 +312,13 @@ H5vccStorageSetQuotaResponse H5vccStorage::SetQuota( void H5vccStorage::SetAndSaveQuotaForBackend(disk_cache::ResourceType type, uint32_t bytes) { + if (disk_cache::settings::GetQuota(type) == bytes) { + return; + } + disk_cache::settings::SetQuota(type, bytes); + network_module_->url_request_context()->UpdateCacheSizeSetting(type, bytes); if (cache_backend_) { - if (cache_backend_->UpdateSizes(type, bytes)) { - auto url_request_context = network_module_->url_request_context(); - url_request_context->UpdateCacheSizeSetting(type, bytes); - } + cache_backend_->UpdateSizes(type, bytes); if (bytes == 0) { network_module_->task_runner()->PostTask( @@ -335,21 +336,19 @@ H5vccStorageResourceTypeQuotaBytesDictionary H5vccStorage::GetQuota() { return quota; } - quota.set_other(cache_backend_->GetQuota(disk_cache::kOther)); - quota.set_html(cache_backend_->GetQuota(disk_cache::kHTML)); - quota.set_css(cache_backend_->GetQuota(disk_cache::kCSS)); - quota.set_image(cache_backend_->GetQuota(disk_cache::kImage)); - quota.set_font(cache_backend_->GetQuota(disk_cache::kFont)); - quota.set_splash(cache_backend_->GetQuota(disk_cache::kSplashScreen)); + quota.set_other(disk_cache::settings::GetQuota(disk_cache::kOther)); + quota.set_html(disk_cache::settings::GetQuota(disk_cache::kHTML)); + quota.set_css(disk_cache::settings::GetQuota(disk_cache::kCSS)); + quota.set_image(disk_cache::settings::GetQuota(disk_cache::kImage)); + quota.set_font(disk_cache::settings::GetQuota(disk_cache::kFont)); + quota.set_splash(disk_cache::settings::GetQuota(disk_cache::kSplashScreen)); quota.set_uncompiled_js( - cache_backend_->GetQuota(disk_cache::kUncompiledScript)); + disk_cache::settings::GetQuota(disk_cache::kUncompiledScript)); quota.set_compiled_js( - cobalt::cache::Cache::GetInstance() - ->GetMaxCacheStorageInBytes(disk_cache::kCompiledScript) - .value()); - quota.set_cache_api(cache_backend_->GetQuota(disk_cache::kCacheApi)); + disk_cache::settings::GetQuota(disk_cache::kCompiledScript)); + quota.set_cache_api(disk_cache::settings::GetQuota(disk_cache::kCacheApi)); quota.set_service_worker_js( - cache_backend_->GetQuota(disk_cache::kServiceWorkerScript)); + disk_cache::settings::GetQuota(disk_cache::kServiceWorkerScript)); uint32_t max_quota_size = 24 * 1024 * 1024; #if SB_API_VERSION >= 14 @@ -369,7 +368,7 @@ void H5vccStorage::EnableCache() { disk_cache::kCacheEnabledPersistentSettingsKey, std::make_unique(true)); - cobalt::cache::Cache::GetInstance()->set_enabled(true); + disk_cache::settings::SetCacheEnabled(true); if (http_cache_) { http_cache_->set_mode(net::HttpCache::Mode::NORMAL); @@ -381,7 +380,7 @@ void H5vccStorage::DisableCache() { disk_cache::kCacheEnabledPersistentSettingsKey, std::make_unique(false)); - cobalt::cache::Cache::GetInstance()->set_enabled(false); + disk_cache::settings::SetCacheEnabled(false); if (http_cache_) { http_cache_->set_mode(net::HttpCache::Mode::DISABLE); diff --git a/cobalt/network/url_request_context.cc b/cobalt/network/url_request_context.cc index 219d97af2899..2efd31e9c864 100644 --- a/cobalt/network/url_request_context.cc +++ b/cobalt/network/url_request_context.cc @@ -15,6 +15,7 @@ #include "cobalt/network/url_request_context.h" #include +#include #include #include #include @@ -53,39 +54,43 @@ namespace { const char kPersistentSettingsJson[] = "cache_settings.json"; - -void ReadDiskCacheSize(cobalt::persistent_storage::PersistentSettings* settings, - int64_t max_bytes) { +void LoadDiskCacheQuotaSettings( + cobalt::persistent_storage::PersistentSettings* settings, + int64_t max_bytes) { auto total_size = 0; - disk_cache::ResourceTypeMetadata kTypeMetadataNew[disk_cache::kTypeCount]; - + std::map quotas; for (int i = 0; i < disk_cache::kTypeCount; i++) { - auto metadata = disk_cache::kTypeMetadata[i]; + disk_cache::ResourceType resource_type = (disk_cache::ResourceType)i; + std::string directory = + disk_cache::defaults::GetSubdirectory(resource_type); uint32_t bucket_size = static_cast(settings->GetPersistentSettingAsDouble( - metadata.directory, metadata.max_size_bytes)); - kTypeMetadataNew[i] = {metadata.directory, bucket_size}; - + directory, disk_cache::defaults::GetQuota(resource_type))); + quotas[resource_type] = bucket_size; total_size += bucket_size; } - // Check if PersistentSettings values are valid and can replace the - // disk_cache::kTypeMetadata. if (total_size <= max_bytes) { - std::copy(std::begin(kTypeMetadataNew), std::end(kTypeMetadataNew), - std::begin(disk_cache::kTypeMetadata)); + for (int i = 0; i < disk_cache::kTypeCount; i++) { + disk_cache::ResourceType resource_type = (disk_cache::ResourceType)i; + disk_cache::settings::SetQuota(resource_type, quotas[resource_type]); + } return; } - // PersistentSettings values are invalid and will be replaced by the default - // values in disk_cache::kTypeMetadata. + // Sum of quotas exceeds |max_bytes|. Set quotas to default values. for (int i = 0; i < disk_cache::kTypeCount; i++) { - auto metadata = disk_cache::kTypeMetadata[i]; + disk_cache::ResourceType resource_type = (disk_cache::ResourceType)i; + uint32_t default_quota = disk_cache::defaults::GetQuota(resource_type); + disk_cache::settings::SetQuota(resource_type, default_quota); + std::string directory = + disk_cache::defaults::GetSubdirectory(resource_type); settings->SetPersistentSetting( - metadata.directory, std::make_unique( - static_cast(metadata.max_size_bytes))); + directory, + std::make_unique(static_cast(default_quota))); } } + } // namespace namespace cobalt { @@ -233,7 +238,8 @@ URLRequestContext::URLRequestContext( cache_persistent_settings_ = std::make_unique( kPersistentSettingsJson); - ReadDiskCacheSize(cache_persistent_settings_.get(), max_cache_bytes); + LoadDiskCacheQuotaSettings(cache_persistent_settings_.get(), + max_cache_bytes); auto http_cache = std::make_unique( storage_.http_network_session(), @@ -245,7 +251,7 @@ URLRequestContext::URLRequestContext( if (persistent_settings != nullptr) { auto cache_enabled = persistent_settings->GetPersistentSettingAsBool( disk_cache::kCacheEnabledPersistentSettingsKey, true); - + disk_cache::settings::SetCacheEnabled(cache_enabled); if (!cache_enabled) { http_cache->set_mode(net::HttpCache::Mode::DISABLE); } @@ -293,7 +299,7 @@ void URLRequestContext::UpdateCacheSizeSetting(disk_cache::ResourceType type, uint32_t bytes) { CHECK(cache_persistent_settings_); cache_persistent_settings_->SetPersistentSetting( - disk_cache::kTypeMetadata[type].directory, + disk_cache::defaults::GetSubdirectory(type), std::make_unique(static_cast(bytes))); } diff --git a/cobalt/persistent_storage/persistent_settings.cc b/cobalt/persistent_storage/persistent_settings.cc index 52bc86432414..2f2e5dd3d93b 100644 --- a/cobalt/persistent_storage/persistent_settings.cc +++ b/cobalt/persistent_storage/persistent_settings.cc @@ -129,7 +129,9 @@ double PersistentSettings::GetPersistentSettingAsDouble( base::AutoLock auto_lock(pref_store_lock_); auto persistent_settings = pref_store_->GetValues(); const base::Value* result = persistent_settings->FindKey(key); - if (result && result->is_double()) return result->GetDouble(); + if (result && result->is_double()) { + return result->GetDouble(); + } return default_setting; } diff --git a/net/BUILD.gn b/net/BUILD.gn index 833e724c097a..2fb59e756818 100644 --- a/net/BUILD.gn +++ b/net/BUILD.gn @@ -427,6 +427,7 @@ component("net") { "disk_cache/cobalt/cobalt_disk_cache.cc", "disk_cache/cobalt/cobalt_backend_impl.cc", "disk_cache/cobalt/cobalt_backend_impl.h", + "disk_cache/cobalt/resource_type.cc", "disk_cache/cobalt/resource_type.h", "disk_cache/net_log_parameters.cc", "disk_cache/net_log_parameters.h", diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.cc b/net/disk_cache/cobalt/cobalt_backend_impl.cc index 6a2ec1c74874..1bf9108d61c9 100644 --- a/net/disk_cache/cobalt/cobalt_backend_impl.cc +++ b/net/disk_cache/cobalt/cobalt_backend_impl.cc @@ -51,6 +51,25 @@ ResourceType GetType(const std::string& key) { return kOther; } +bool NeedsBackend(ResourceType resource_type) { + switch (resource_type) { + case kHTML: + case kCSS: + case kImage: + case kFont: + case kUncompiledScript: + case kOther: + case kSplashScreen: + return true; + case kCompiledScript: + case kCacheApi: + case kServiceWorkerScript: + default: + return false; + } + return false; +} + } // namespace CobaltBackendImpl::CobaltBackendImpl( @@ -60,44 +79,41 @@ CobaltBackendImpl::CobaltBackendImpl( net::CacheType cache_type, net::NetLog* net_log) : weak_factory_(this) { - - // Initialize disk backend for each resource type. - // Note: kTypeMetadata is refreshed from settings before this constructor runs int64_t total_size = 0; for (int i = 0; i < kTypeCount; i++) { - auto metadata = kTypeMetadata[i]; - base::FilePath dir = path.Append(FILE_PATH_LITERAL(metadata.directory)); - int64_t bucket_size = metadata.max_size_bytes; + ResourceType resource_type = (ResourceType)i; + if (!NeedsBackend(resource_type)) { + continue; + } + std::string sub_directory = defaults::GetSubdirectory(resource_type); + base::FilePath dir = path.Append(FILE_PATH_LITERAL(sub_directory)); + uint32_t bucket_size = disk_cache::settings::GetQuota(resource_type); total_size += bucket_size; SimpleBackendImpl* simple_backend = new SimpleBackendImpl( - dir, cleanup_tracker, /* file_tracker = */ nullptr, bucket_size, + dir, cleanup_tracker, /*file_tracker=*/nullptr, bucket_size, cache_type, net_log); - simple_backend_map_[(ResourceType)i] = simple_backend; + simple_backend_map_[resource_type] = simple_backend; } - // Must be at least enough space for each backend. DCHECK(total_size <= max_bytes); } CobaltBackendImpl::~CobaltBackendImpl() { for (int i = 0; i < kTypeCount; i++) { - delete simple_backend_map_[(ResourceType)i]; + ResourceType resource_type = (ResourceType)i; + if (simple_backend_map_.count(resource_type) == 1) { + delete simple_backend_map_[resource_type]; + } } simple_backend_map_.clear(); } -bool CobaltBackendImpl::UpdateSizes(ResourceType type, uint32_t bytes) { - if (bytes == disk_cache::kTypeMetadata[type].max_size_bytes) - return false; - - disk_cache::kTypeMetadata[type].max_size_bytes = bytes; +void CobaltBackendImpl::UpdateSizes(ResourceType type, uint32_t bytes) { + if (simple_backend_map_.count(type) == 0) { + return; + } SimpleBackendImpl* simple_backend = simple_backend_map_[type]; simple_backend->SetMaxSize(bytes); - return true; -} - -uint32_t CobaltBackendImpl::GetQuota(ResourceType type) { - return disk_cache::kTypeMetadata[type].max_size_bytes; } net::Error CobaltBackendImpl::Init(CompletionOnceCallback completion_callback) { @@ -128,6 +144,9 @@ net::Error CobaltBackendImpl::OpenEntry(const std::string& key, net::RequestPriority request_priority, Entry** entry, CompletionOnceCallback callback) { + if (simple_backend_map_.count(GetType(key)) == 0) { + return net::Error::ERR_BLOCKED_BY_CLIENT; + } SimpleBackendImpl* simple_backend = simple_backend_map_[GetType(key)]; return simple_backend->OpenEntry(key, request_priority, entry, std::move(callback)); @@ -138,11 +157,11 @@ net::Error CobaltBackendImpl::CreateEntry(const std::string& key, Entry** entry, CompletionOnceCallback callback) { ResourceType type = GetType(key); - auto quota = disk_cache::kTypeMetadata[type].max_size_bytes; - if (quota == 0) { + auto quota = disk_cache::settings::GetQuota(type); + if (quota == 0 || simple_backend_map_.count(type) == 0) { return net::Error::ERR_BLOCKED_BY_CLIENT; } - SimpleBackendImpl* simple_backend = simple_backend_map_[GetType(key)]; + SimpleBackendImpl* simple_backend = simple_backend_map_[type]; return simple_backend->CreateEntry(key, request_priority, entry, std::move(callback)); } @@ -150,6 +169,9 @@ net::Error CobaltBackendImpl::CreateEntry(const std::string& key, net::Error CobaltBackendImpl::DoomEntry(const std::string& key, net::RequestPriority priority, CompletionOnceCallback callback) { + if (simple_backend_map_.count(GetType(key)) == 0) { + return net::Error::ERR_BLOCKED_BY_CLIENT; + } SimpleBackendImpl* simple_backend = simple_backend_map_[GetType(key)]; return simple_backend->DoomEntry(key, priority, std::move(callback)); } @@ -228,6 +250,9 @@ std::unique_ptr CobaltBackendImpl::CreateIterator() { } void CobaltBackendImpl::OnExternalCacheHit(const std::string& key) { + if (simple_backend_map_.count(GetType(key)) == 0) { + return; + } SimpleBackendImpl* simple_backend = simple_backend_map_[GetType(key)]; simple_backend->OnExternalCacheHit(key); } @@ -246,6 +271,10 @@ size_t CobaltBackendImpl::DumpMemoryStats( net::Error CobaltBackendImpl::DoomAllEntriesOfType(disk_cache::ResourceType type, CompletionOnceCallback callback) { + if (simple_backend_map_.count(type) == 0) { + std::move(callback).Run(net::OK); + return net::OK; + } SimpleBackendImpl* simple_backend = simple_backend_map_[type]; return simple_backend->DoomAllEntries(std::move(callback)); } diff --git a/net/disk_cache/cobalt/cobalt_backend_impl.h b/net/disk_cache/cobalt/cobalt_backend_impl.h index effa70bd7d74..b1bfe31f5167 100644 --- a/net/disk_cache/cobalt/cobalt_backend_impl.h +++ b/net/disk_cache/cobalt/cobalt_backend_impl.h @@ -48,8 +48,7 @@ class NET_EXPORT_PRIVATE CobaltBackendImpl final : public Backend { ~CobaltBackendImpl() override; net::Error Init(CompletionOnceCallback completion_callback); - bool UpdateSizes(ResourceType type, uint32_t bytes); - uint32_t GetQuota(ResourceType type); + void UpdateSizes(ResourceType type, uint32_t bytes); // Backend interface. net::CacheType GetCacheType() const override; @@ -115,7 +114,6 @@ class NET_EXPORT_PRIVATE CobaltBackendImpl final : public Backend { base::WeakPtrFactory weak_factory_; std::map simple_backend_map_; - }; } // namespace disk_cache diff --git a/net/disk_cache/cobalt/resource_type.cc b/net/disk_cache/cobalt/resource_type.cc new file mode 100644 index 000000000000..6a3f2f37b1dc --- /dev/null +++ b/net/disk_cache/cobalt/resource_type.cc @@ -0,0 +1,175 @@ +// Copyright 2023 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. + +#include "net/disk_cache/cobalt/resource_type.h" + +#include "base/logging.h" + +namespace disk_cache { +namespace defaults { + +std::string GetSubdirectory(ResourceType resource_type) { + switch (resource_type) { + case kOther: + return "other"; + case kHTML: + return "html"; + case kCSS: + return "css"; + case kImage: + return "image"; + case kFont: + return "font"; + case kSplashScreen: + return "splash"; + case kUncompiledScript: + return "uncompiled_js"; + case kCompiledScript: + return "compiled_js"; + case kCacheApi: + return "cache_api"; + case kServiceWorkerScript: + return "service_worker_js"; + default: + NOTREACHED() << "Unexpected resource_type " << resource_type; + break; + } + return ""; +} + +uint32_t GetQuota(ResourceType resource_type) { + switch (resource_type) { + case kOther: + return 3 * 1024 * 1024; + case kHTML: + return 2 * 1024 * 1024; + case kCSS: + return 1 * 1024 * 1024; + case kImage: + return 0; + case kFont: + return 3 * 1024 * 1024; + case kSplashScreen: + return 2 * 1024 * 1024; + case kUncompiledScript: + return 3 * 1024 * 1024; + case kCompiledScript: + return 3 * 1024 * 1024; + case kCacheApi: + return 3 * 1024 * 1024; + case kServiceWorkerScript: + return 3 * 1024 * 1024; + default: + NOTREACHED() << "Unexpected resource_type " << resource_type; + break; + } + return 0; +} + +} // namespace defaults + +namespace settings { + +namespace { + +starboard::atomic_int32_t other_quota = starboard::atomic_int32_t(defaults::GetQuota(kOther)); +starboard::atomic_int32_t html_quota = starboard::atomic_int32_t(defaults::GetQuota(kHTML)); +starboard::atomic_int32_t css_quota = starboard::atomic_int32_t(defaults::GetQuota(kCSS)); +starboard::atomic_int32_t image_quota = starboard::atomic_int32_t(defaults::GetQuota(kImage)); +starboard::atomic_int32_t font_quota = starboard::atomic_int32_t(defaults::GetQuota(kFont)); +starboard::atomic_int32_t splash_screen_quota = starboard::atomic_int32_t(defaults::GetQuota(kSplashScreen)); +starboard::atomic_int32_t uncompiled_script_quota = starboard::atomic_int32_t(defaults::GetQuota(kUncompiledScript)); +starboard::atomic_int32_t compiled_script_quota = starboard::atomic_int32_t(defaults::GetQuota(kCompiledScript)); +starboard::atomic_int32_t cache_api_quota = starboard::atomic_int32_t(defaults::GetQuota(kCacheApi)); +starboard::atomic_int32_t service_worker_script_quota = starboard::atomic_int32_t(defaults::GetQuota(kServiceWorkerScript)); +starboard::atomic_bool cache_enabled = starboard::atomic_bool(true); + +} // namespace + +uint32_t GetQuota(ResourceType resource_type) { + switch (resource_type) { + case kOther: + return other_quota.load(); + case kHTML: + return html_quota.load(); + case kCSS: + return css_quota.load(); + case kImage: + return image_quota.load(); + case kFont: + return font_quota.load(); + case kSplashScreen: + return splash_screen_quota.load(); + case kUncompiledScript: + return uncompiled_script_quota.load(); + case kCompiledScript: + return compiled_script_quota.load(); + case kCacheApi: + return cache_api_quota.load(); + case kServiceWorkerScript: + return service_worker_script_quota.load(); + default: + NOTREACHED() << "Unexpected resource_type " << resource_type; + } + return 0; +} + +void SetQuota(ResourceType resource_type, uint32_t value) { + switch (resource_type) { + case kOther: + other_quota.store(static_cast(value)); + break; + case kHTML: + html_quota.store(static_cast(value)); + break; + case kCSS: + css_quota.store(static_cast(value)); + break; + case kImage: + image_quota.store(static_cast(value)); + break; + case kFont: + font_quota.store(static_cast(value)); + break; + case kSplashScreen: + splash_screen_quota.store(static_cast(value)); + break; + case kUncompiledScript: + uncompiled_script_quota.store(static_cast(value)); + break; + case kCompiledScript: + compiled_script_quota.store(static_cast(value)); + break; + case kCacheApi: + cache_api_quota.store(static_cast(value)); + break; + case kServiceWorkerScript: + service_worker_script_quota.store(static_cast(value)); + break; + default: + NOTREACHED() << "Unexpected resource_type " << resource_type; + break; + } +} + +bool GetCacheEnabled() { + return cache_enabled.load(); +} + +void SetCacheEnabled(bool value) { + cache_enabled.store(value); +} + +} // namespace settings +} // namespace disk_cache diff --git a/net/disk_cache/cobalt/resource_type.h b/net/disk_cache/cobalt/resource_type.h index f291033543a2..f401b65dbd7e 100644 --- a/net/disk_cache/cobalt/resource_type.h +++ b/net/disk_cache/cobalt/resource_type.h @@ -17,6 +17,9 @@ #include +#include "starboard/common/atomic.h" +#include "starboard/types.h" + namespace disk_cache { /* Note: If adding a new resource type, add corresponding metadata below. */ @@ -34,22 +37,21 @@ enum ResourceType { kTypeCount = 10, }; -struct ResourceTypeMetadata { - std::string directory; - uint32_t max_size_bytes; -}; +namespace defaults { -static uint32_t kInitialBytes = static_cast (3 * 1024 * 1024); -// These values are updated on start up in application.cc, using the -// persisted values saved in settings.json. -static ResourceTypeMetadata kTypeMetadata[] = { - {"other", kInitialBytes}, {"html", 2 * 1024 * 1024}, - {"css", 1 * 1024 * 1024}, {"image", 0}, - {"font", kInitialBytes}, {"splash", 2 * 1024 * 1024}, - {"uncompiled_js", kInitialBytes}, {"compiled_js", kInitialBytes}, - {"cache_api", kInitialBytes}, {"service_worker_js", kInitialBytes}, -}; +std::string GetSubdirectory(ResourceType resource_type); +uint32_t GetQuota(ResourceType resource_type); + +} // namespace defaults + +namespace settings { + +uint32_t GetQuota(ResourceType resource_type); +void SetQuota(ResourceType resource_type, uint32_t value); +bool GetCacheEnabled(); +void SetCacheEnabled(bool value); +} // namespace settings } // namespace disk_cache #endif // NET_DISK_CACHE_COBALT_RESOURCE_TYPE_H_ diff --git a/net/disk_cache/disk_cache.cc b/net/disk_cache/disk_cache.cc index b8abfac5d129..81457ca5751b 100644 --- a/net/disk_cache/disk_cache.cc +++ b/net/disk_cache/disk_cache.cc @@ -269,7 +269,8 @@ net::Error CreateCacheBackend(net::CacheType type, #if defined(OS_ANDROID) nullptr, #endif - net_log, backend, base::OnceClosure(), + net_log, backend, + base::OnceClosure(), std::move(callback)); } @@ -286,7 +287,8 @@ NET_EXPORT net::Error CreateCacheBackend( base::android::ApplicationStatusListener* app_status_listener) { return CreateCacheBackendImpl(type, backend_type, path, max_bytes, force, std::move(app_status_listener), net_log, - backend, base::OnceClosure(), + backend, + base::OnceClosure(), std::move(callback)); } #endif From 0f1060b4cb2475409ccd076fbe0c3dee81752602 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 19 Oct 2023 13:51:06 -0700 Subject: [PATCH 109/140] Cherry pick PR #1737: [android] Avoid underrun if audio output changes (#1823) Refer to the original PR: https://github.com/youtube/cobalt/pull/1737 The cl makes SbAudioSinkGetMinBufferSizeInFrames() return default values if audio output type changes from remote to local or from local to remote. It doesn't re-run MinRequiredFramesTester. b/298847156 Co-authored-by: Jason --- .../shared/audio_track_audio_sink_type.cc | 81 +++++++++++++++---- .../shared/audio_track_audio_sink_type.h | 1 + 2 files changed, 66 insertions(+), 16 deletions(-) diff --git a/starboard/android/shared/audio_track_audio_sink_type.cc b/starboard/android/shared/audio_track_audio_sink_type.cc index ada20e9b8a39..98810e75db4d 100644 --- a/starboard/android/shared/audio_track_audio_sink_type.cc +++ b/starboard/android/shared/audio_track_audio_sink_type.cc @@ -18,6 +18,7 @@ #include #include +#include "starboard/android/shared/media_capabilities_cache.h" #include "starboard/common/string.h" #include "starboard/shared/starboard/media/media_util.h" #include "starboard/shared/starboard/player/filter/common.h" @@ -50,8 +51,10 @@ const SbTime kMaxDurationPerRequestInTunnelMode = 16 * kSbTimeMillisecond; const size_t kSilenceFramesPerAppend = 1024; -const int kMaxRequiredFrames = 16 * 1024; -const int kRequiredFramesIncrement = 2 * 1024; +const int kMaxRequiredFramesLocal = 16 * 1024; +const int kMaxRequiredFramesRemote = 32 * 1024; +const int kMaxRequiredFrames = kMaxRequiredFramesRemote; +const int kRequiredFramesIncrement = 4 * 1024; const int kMinStablePlayedFrames = 12 * 1024; const int kSampleFrequency22050 = 22050; @@ -67,6 +70,38 @@ int GetMaxFramesPerRequestForTunnelMode(int sampling_frequency_hz) { return (max_frames + 15) / 16 * 16; // align to 16 } +bool HasRemoteAudioOutput() { +#if SB_API_VERSION >= 15 + // SbPlayerBridge::GetAudioConfigurations() reads up to 32 configurations. The + // limit here is to avoid infinite loop and also match + // SbPlayerBridge::GetAudioConfigurations(). + const int kMaxAudioConfigurations = 32; + SbMediaAudioConfiguration configuration; + int index = 0; + while (index < kMaxAudioConfigurations && + MediaCapabilitiesCache::GetInstance()->GetAudioConfiguration( + index, &configuration)) { + switch (configuration.connector) { + case kSbMediaAudioConnectorUnknown: + case kSbMediaAudioConnectorAnalog: + case kSbMediaAudioConnectorBuiltIn: + case kSbMediaAudioConnectorHdmi: + case kSbMediaAudioConnectorSpdif: + case kSbMediaAudioConnectorUsb: + break; + case kSbMediaAudioConnectorBluetooth: + case kSbMediaAudioConnectorRemoteWired: + case kSbMediaAudioConnectorRemoteWireless: + case kSbMediaAudioConnectorRemoteOther: + return true; + } + index++; + } + return false; +#endif // SB_API_VERSION >= 15 + return false; +} + } // namespace AudioTrackAudioSink::AudioTrackAudioSink( @@ -487,11 +522,18 @@ void AudioTrackAudioSinkType::TestMinRequiredFrames() { auto onMinRequiredFramesForWebAudioReceived = [&](int number_of_channels, SbMediaAudioSampleType sample_type, int sample_rate, int min_required_frames) { + bool has_remote_audio_output = HasRemoteAudioOutput(); SB_LOG(INFO) << "Received min required frames " << min_required_frames << " for " << number_of_channels << " channels, " - << sample_rate << "hz."; + << sample_rate << "hz, with " + << (has_remote_audio_output ? "remote" : "local") + << " audio output device."; ScopedLock lock(min_required_frames_map_mutex_); - min_required_frames_map_[sample_rate] = min_required_frames; + has_remote_audio_output_ = has_remote_audio_output; + min_required_frames_map_[sample_rate] = + std::min(min_required_frames, has_remote_audio_output_ + ? kMaxRequiredFramesRemote + : kMaxRequiredFramesLocal); }; SbMediaAudioSampleType sample_type = kSbMediaAudioSampleTypeFloat32; @@ -512,20 +554,27 @@ int AudioTrackAudioSinkType::GetMinBufferSizeInFramesInternal( int channels, SbMediaAudioSampleType sample_type, int sampling_frequency_hz) { - if (sampling_frequency_hz <= kSampleFrequency22050) { - ScopedLock lock(min_required_frames_map_mutex_); - if (min_required_frames_map_.find(kSampleFrequency22050) != - min_required_frames_map_.end()) { - return min_required_frames_map_[kSampleFrequency22050]; - } - } else if (sampling_frequency_hz <= kSampleFrequency48000) { - ScopedLock lock(min_required_frames_map_mutex_); - if (min_required_frames_map_.find(kSampleFrequency48000) != - min_required_frames_map_.end()) { - return min_required_frames_map_[kSampleFrequency48000]; + bool has_remote_audio_output = HasRemoteAudioOutput(); + ScopedLock lock(min_required_frames_map_mutex_); + if (has_remote_audio_output == has_remote_audio_output_) { + // There's no audio output type change, we can use the numbers we got from + // the tests at app launch. + if (sampling_frequency_hz <= kSampleFrequency22050) { + if (min_required_frames_map_.find(kSampleFrequency22050) != + min_required_frames_map_.end()) { + return min_required_frames_map_[kSampleFrequency22050]; + } + } else if (sampling_frequency_hz <= kSampleFrequency48000) { + if (min_required_frames_map_.find(kSampleFrequency48000) != + min_required_frames_map_.end()) { + return min_required_frames_map_[kSampleFrequency48000]; + } } } - return kMaxRequiredFrames; + // We cannot find a matched result from our tests, or the audio output type + // has changed. We use the default max required frames to avoid underruns. + return has_remote_audio_output ? kMaxRequiredFramesRemote + : kMaxRequiredFramesLocal; } } // namespace shared diff --git a/starboard/android/shared/audio_track_audio_sink_type.h b/starboard/android/shared/audio_track_audio_sink_type.h index 88974ff2a69e..b115debcec78 100644 --- a/starboard/android/shared/audio_track_audio_sink_type.h +++ b/starboard/android/shared/audio_track_audio_sink_type.h @@ -97,6 +97,7 @@ class AudioTrackAudioSinkType : public SbAudioSinkPrivate::Type { // The minimum frames required to avoid underruns of different frequencies. std::map min_required_frames_map_; MinRequiredFramesTester min_required_frames_tester_; + bool has_remote_audio_output_ = false; }; class AudioTrackAudioSink : public SbAudioSinkPrivate { From 798e8d7ed2530435363b8e5338f9c2180a925ad9 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 19 Oct 2023 14:57:02 -0700 Subject: [PATCH 110/140] Cherry pick PR #1806: [XB1] Fix non-internal builds (#1814) Refer to the original PR: https://github.com/youtube/cobalt/pull/1806 b/305285508 Change-Id: Iee0da0c85bba750d8929eacf60e45c1797e354af Co-authored-by: Tyler Holcombe --- starboard/shared/uwp/extended_resources_manager.cc | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/starboard/shared/uwp/extended_resources_manager.cc b/starboard/shared/uwp/extended_resources_manager.cc index 08f20da3c4ec..4d53de84bca5 100644 --- a/starboard/shared/uwp/extended_resources_manager.cc +++ b/starboard/shared/uwp/extended_resources_manager.cc @@ -42,9 +42,9 @@ using ::starboard::shared::starboard::media::MimeSupportabilityCache; using Windows::Foundation::Metadata::ApiInformation; #if defined(INTERNAL_BUILD) using ::starboard::xb1::shared::Av1VideoDecoder; +using ::starboard::xb1::shared::GpuVideoDecoderBase; using ::starboard::xb1::shared::VpxVideoDecoder; #endif // defined(INTERNAL_BUILD) -using ::starboard::xb1::shared::GpuVideoDecoderBase; const SbTime kReleaseTimeout = kSbTimeSecond; @@ -473,8 +473,10 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { } else { SB_LOG(INFO) << "CreateFence() failed with " << hr; } +#if defined(INTERNAL_BUILD) // Clear frame buffers used for rendering queue GpuVideoDecoderBase::ClearFrameBuffersPool(); +#endif // #if defined(INTERNAL_BUILD) } if (d3d12queue_) { From c0533edfa9491969d53691613e21e87b246e4421 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 19 Oct 2023 15:19:26 -0700 Subject: [PATCH 111/140] Cherry pick PR #357: Add synchronization to wait extended resources acquiring (#1298) Refer to the original PR: https://github.com/youtube/cobalt/pull/357 b/176502184 Replaced IsGpuDecoderReady() by WaitGpuDecoderReady() when check if video supported Co-authored-by: victorpasoshnikov <133087930+victorpasoshnikov@users.noreply.github.com> --- .../player/filter/testing/test_util.cc | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/starboard/shared/starboard/player/filter/testing/test_util.cc b/starboard/shared/starboard/player/filter/testing/test_util.cc index 25ad28129975..39ae17dff578 100644 --- a/starboard/shared/starboard/player/filter/testing/test_util.cc +++ b/starboard/shared/starboard/player/filter/testing/test_util.cc @@ -181,14 +181,32 @@ std::vector GetSupportedVideoTests() { const auto& video_stream_info = dmp_reader.video_stream_info(); const std::string video_mime = dmp_reader.video_mime_type(); const MimeType video_mime_type(video_mime.c_str()); - if (SbMediaIsVideoSupported( - dmp_reader.video_codec(), - video_mime.size() > 0 ? &video_mime_type : nullptr, -1, -1, 8, - kSbMediaPrimaryIdUnspecified, kSbMediaTransferIdUnspecified, - kSbMediaMatrixIdUnspecified, video_stream_info.frame_width, - video_stream_info.frame_height, dmp_reader.video_bitrate(), - dmp_reader.video_fps(), false)) { - test_params.push_back(std::make_tuple(filename, output_mode)); + // SbMediaIsVideoSupported may return false for gpu based decoder that in + // fact supports av1 or/and vp9 because the system can make async + // initialization at startup. + // To minimize probability of false negative we check result few times + static bool decoder_has_been_checked_once = false; + int counter = 5; + const SbMediaVideoCodec video_codec = dmp_reader.video_codec(); + bool need_to_check_with_wait = video_codec == kSbMediaVideoCodecAv1 || + video_codec == kSbMediaVideoCodecVp9; + do { + if (SbMediaIsVideoSupported( + video_codec, video_mime.size() > 0 ? &video_mime_type : nullptr, + -1, -1, 8, kSbMediaPrimaryIdUnspecified, + kSbMediaTransferIdUnspecified, kSbMediaMatrixIdUnspecified, + video_stream_info.frame_width, video_stream_info.frame_height, + dmp_reader.video_bitrate(), dmp_reader.video_fps(), false)) { + test_params.push_back(std::make_tuple(filename, output_mode)); + break; + } else if (need_to_check_with_wait && !decoder_has_been_checked_once) { + SbThreadSleep(kSbTimeSecond); + } else { + break; + } + } while (--counter); + if (need_to_check_with_wait) { + decoder_has_been_checked_once = true; } } } From 83a728948e7227ed782cdf92f64f1c7877508c67 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 20 Oct 2023 11:36:01 -0700 Subject: [PATCH 112/140] Cherry pick PR #1798: Add NetLog record method to Cobalt panel in Devtools (#1828) Refer to the original PR: https://github.com/youtube/cobalt/pull/1798 Much like the utility button to capture and download a Trace, this CL adds a button to the Cobalt tab of Devtools for capturing a NetLog. View it here - https://screenshot.googleplex.com/ATA2L2GcEH6W272 See here for more details on NetLog - https://github.com/youtube/cobalt/blob/main/cobalt/doc/net_log.md b/305257093 Co-authored-by: thorsten sideb0ard --- cobalt/browser/idl_files.gni | 1 + cobalt/h5vcc/BUILD.gn | 2 + cobalt/h5vcc/h5vcc.cc | 2 + cobalt/h5vcc/h5vcc.h | 3 ++ cobalt/h5vcc/h5vcc.idl | 1 + cobalt/h5vcc/h5vcc_net_log.cc | 43 ++++++++++++++++++ cobalt/h5vcc/h5vcc_net_log.h | 45 +++++++++++++++++++ cobalt/h5vcc/h5vcc_net_log.idl | 19 ++++++++ cobalt/network/cobalt_net_log.cc | 25 ++++++++--- cobalt/network/cobalt_net_log.h | 7 ++- cobalt/network/network_module.cc | 32 ++++++++++--- cobalt/network/network_module.h | 9 +++- .../devtools/front_end/cobalt/cobalt.js | 28 ++++++++++++ 13 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 cobalt/h5vcc/h5vcc_net_log.cc create mode 100644 cobalt/h5vcc/h5vcc_net_log.h create mode 100644 cobalt/h5vcc/h5vcc_net_log.idl diff --git a/cobalt/browser/idl_files.gni b/cobalt/browser/idl_files.gni index 4663d09bcdaa..41539c3ff62c 100644 --- a/cobalt/browser/idl_files.gni +++ b/cobalt/browser/idl_files.gni @@ -171,6 +171,7 @@ source_idl_files = [ "//cobalt/h5vcc/h5vcc_screen.idl", "//cobalt/h5vcc/h5vcc_system.idl", "//cobalt/h5vcc/h5vcc_trace_event.idl", + "//cobalt/h5vcc/h5vcc_net_log.idl", "//cobalt/h5vcc/h5vcc_updater.idl", "//cobalt/media_capture/blob_event.idl", diff --git a/cobalt/h5vcc/BUILD.gn b/cobalt/h5vcc/BUILD.gn index 901f668cfa6d..1de435680168 100644 --- a/cobalt/h5vcc/BUILD.gn +++ b/cobalt/h5vcc/BUILD.gn @@ -45,6 +45,8 @@ static_library("h5vcc") { "h5vcc_event_listener_container.h", "h5vcc_metrics.cc", "h5vcc_metrics.h", + "h5vcc_net_log.cc", + "h5vcc_net_log.h", "h5vcc_platform_service.cc", "h5vcc_platform_service.h", "h5vcc_runtime.cc", diff --git a/cobalt/h5vcc/h5vcc.cc b/cobalt/h5vcc/h5vcc.cc index 5d4d43a879a7..ebf7b568be2f 100644 --- a/cobalt/h5vcc/h5vcc.cc +++ b/cobalt/h5vcc/h5vcc.cc @@ -45,6 +45,7 @@ H5vcc::H5vcc(const Settings& settings) { storage_ = new H5vccStorage(settings.network_module, settings.persistent_settings); trace_event_ = new H5vccTraceEvent(); + net_log_ = new H5vccNetLog(settings.network_module); #if SB_IS(EVERGREEN) updater_ = new H5vccUpdater(settings.updater_module); system_ = new H5vccSystem(updater_); @@ -82,6 +83,7 @@ void H5vcc::TraceMembers(script::Tracer* tracer) { tracer->Trace(storage_); tracer->Trace(system_); tracer->Trace(trace_event_); + tracer->Trace(net_log_); #if SB_IS(EVERGREEN) tracer->Trace(updater_); #endif diff --git a/cobalt/h5vcc/h5vcc.h b/cobalt/h5vcc/h5vcc.h index 0d60a7c61735..0852eaa1a5c0 100644 --- a/cobalt/h5vcc/h5vcc.h +++ b/cobalt/h5vcc/h5vcc.h @@ -26,6 +26,7 @@ #include "cobalt/h5vcc/h5vcc_audio_config_array.h" #include "cobalt/h5vcc/h5vcc_crash_log.h" #include "cobalt/h5vcc/h5vcc_metrics.h" +#include "cobalt/h5vcc/h5vcc_net_log.h" #include "cobalt/h5vcc/h5vcc_runtime.h" #include "cobalt/h5vcc/h5vcc_settings.h" #include "cobalt/h5vcc/h5vcc_storage.h" @@ -89,6 +90,7 @@ class H5vcc : public script::Wrappable { const scoped_refptr& trace_event() const { return trace_event_; } + const scoped_refptr& net_log() const { return net_log_; } #if SB_IS(EVERGREEN) const scoped_refptr& updater() const { return updater_; } #endif @@ -108,6 +110,7 @@ class H5vcc : public script::Wrappable { scoped_refptr storage_; scoped_refptr system_; scoped_refptr trace_event_; + scoped_refptr net_log_; #if SB_IS(EVERGREEN) scoped_refptr updater_; #endif diff --git a/cobalt/h5vcc/h5vcc.idl b/cobalt/h5vcc/h5vcc.idl index 188e66b0bb8c..2beb5dc64b20 100644 --- a/cobalt/h5vcc/h5vcc.idl +++ b/cobalt/h5vcc/h5vcc.idl @@ -42,6 +42,7 @@ interface H5vcc { readonly attribute H5vccStorage storage; readonly attribute H5vccSystem system; readonly attribute H5vccTraceEvent traceEvent; + readonly attribute H5vccNetLog netLog; [Conditional=SB_IS_EVERGREEN] readonly attribute H5vccUpdater updater; }; diff --git a/cobalt/h5vcc/h5vcc_net_log.cc b/cobalt/h5vcc/h5vcc_net_log.cc new file mode 100644 index 000000000000..8a5b3a252ee6 --- /dev/null +++ b/cobalt/h5vcc/h5vcc_net_log.cc @@ -0,0 +1,43 @@ +// Copyright 2023 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. + +#include "cobalt/h5vcc/h5vcc_net_log.h" + +#include "base/files/file_util.h" +#include "base/path_service.h" +#include "cobalt/base/cobalt_paths.h" +#include "cobalt/network/cobalt_net_log.h" + +namespace cobalt { +namespace h5vcc { + +H5vccNetLog::H5vccNetLog(cobalt::network::NetworkModule* network_module) + : network_module_{network_module} {} + +void H5vccNetLog::Start() { network_module_->StartNetLog(); } + +void H5vccNetLog::Stop() { network_module_->StopNetLog(); } + +std::string H5vccNetLog::StopAndRead() { + base::FilePath netlog_path = network_module_->StopNetLog(); + std::string netlog_output{}; + if (!netlog_path.empty()) { + ReadFileToString(netlog_path, &netlog_output); + } + return netlog_output; +} + + +} // namespace h5vcc +} // namespace cobalt diff --git a/cobalt/h5vcc/h5vcc_net_log.h b/cobalt/h5vcc/h5vcc_net_log.h new file mode 100644 index 000000000000..801839b7a152 --- /dev/null +++ b/cobalt/h5vcc/h5vcc_net_log.h @@ -0,0 +1,45 @@ +// Copyright 2023 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 COBALT_H5VCC_H5VCC_NET_LOG_H_ +#define COBALT_H5VCC_H5VCC_NET_LOG_H_ + +#include + +#include "cobalt/network/network_module.h" +#include "cobalt/script/wrappable.h" + +namespace cobalt { +namespace h5vcc { + +class H5vccNetLog : public script::Wrappable { + public: + explicit H5vccNetLog(cobalt::network::NetworkModule* network_module); + + void Start(); + void Stop(); + std::string StopAndRead(); + + DEFINE_WRAPPABLE_TYPE(H5vccNetLog); + + private: + cobalt::network::NetworkModule* network_module_ = nullptr; + + DISALLOW_COPY_AND_ASSIGN(H5vccNetLog); +}; + +} // namespace h5vcc +} // namespace cobalt + +#endif // COBALT_H5VCC_H5VCC_NET_LOG_H_ diff --git a/cobalt/h5vcc/h5vcc_net_log.idl b/cobalt/h5vcc/h5vcc_net_log.idl new file mode 100644 index 000000000000..2c607921682e --- /dev/null +++ b/cobalt/h5vcc/h5vcc_net_log.idl @@ -0,0 +1,19 @@ +// Copyright 2023 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. + +interface H5vccNetLog { + void start(); + void stop(); + DOMString stopAndRead(); +}; diff --git a/cobalt/network/cobalt_net_log.cc b/cobalt/network/cobalt_net_log.cc index 3df4117e1149..f597b8785664 100644 --- a/cobalt/network/cobalt_net_log.cc +++ b/cobalt/network/cobalt_net_log.cc @@ -25,14 +25,29 @@ namespace network { CobaltNetLog::CobaltNetLog(const base::FilePath& log_path, net::NetLogCaptureMode capture_mode) - : net_log_logger_( - net::FileNetLogObserver::CreateUnbounded(log_path, nullptr)) { - net_log_logger_->StartObserving(this, capture_mode); -} + : capture_mode_(capture_mode), + net_log_logger_( + net::FileNetLogObserver::CreateUnbounded(log_path, nullptr)) {} CobaltNetLog::~CobaltNetLog() { // Remove the observers we own before we're destroyed. - net_log_logger_->StopObserving(nullptr, base::OnceClosure()); + StopObserving(); +} + +void CobaltNetLog::StartObserving() { + if (!is_observing_) { + is_observing_ = true; + net_log_logger_->StartObserving(this, capture_mode_); + } else { + DLOG(WARNING) << "Already observing NetLog."; + } +} + +void CobaltNetLog::StopObserving() { + if (is_observing_) { + is_observing_ = false; + net_log_logger_->StopObserving(nullptr, base::OnceClosure()); + } } } // namespace network diff --git a/cobalt/network/cobalt_net_log.h b/cobalt/network/cobalt_net_log.h index 4ee6fa8adc30..dcc1e6765f0d 100644 --- a/cobalt/network/cobalt_net_log.h +++ b/cobalt/network/cobalt_net_log.h @@ -34,8 +34,13 @@ class CobaltNetLog : public ::net::NetLog { ::net::NetLogCaptureMode capture_mode); ~CobaltNetLog() override; + void StartObserving(); + void StopObserving(); + private: - std::unique_ptr net_log_logger_; + bool is_observing_{false}; + net::NetLogCaptureMode capture_mode_; + std::unique_ptr net_log_logger_{nullptr}; DISALLOW_COPY_AND_ASSIGN(CobaltNetLog); }; diff --git a/cobalt/network/network_module.cc b/cobalt/network/network_module.cc index 8f562ea882e6..44cce0d66409 100644 --- a/cobalt/network/network_module.cc +++ b/cobalt/network/network_module.cc @@ -17,10 +17,12 @@ #include "base/bind.h" #include "base/command_line.h" #include "base/logging.h" +#include "base/path_service.h" #include "base/strings/string_number_conversions.h" #include "base/strings/stringprintf.h" #include "base/synchronization/waitable_event.h" #include "base/threading/thread.h" +#include "cobalt/base/cobalt_paths.h" #include "cobalt/network/network_system.h" #include "cobalt/network/switches.h" #include "net/url_request/static_http_user_agent_settings.h" @@ -33,6 +35,7 @@ namespace { const char kCaptureModeIncludeCookiesAndCredentials[] = "IncludeCookiesAndCredentials"; const char kCaptureModeIncludeSocketBytes[] = "IncludeSocketBytes"; +const char kDefaultNetLogName[] = "cobalt_netlog.json"; #endif } // namespace @@ -159,11 +162,12 @@ void NetworkModule::Initialize(const std::string& user_agent_string, } #if defined(ENABLE_NETWORK_LOGGING) + base::FilePath result; + base::PathService::Get(cobalt::paths::DIR_COBALT_DEBUG_OUT, &result); + net_log_path_ = result.Append(kDefaultNetLogName); + net::NetLogCaptureMode capture_mode; if (command_line->HasSwitch(switches::kNetLog)) { - // If this is not a valid path, net logs will be sent to VLOG(1). - base::FilePath net_log_path = - command_line->GetSwitchValuePath(switches::kNetLog); - net::NetLogCaptureMode capture_mode; + net_log_path_ = command_line->GetSwitchValuePath(switches::kNetLog); if (command_line->HasSwitch(switches::kNetLogCaptureMode)) { std::string capture_mode_string = command_line->GetSwitchValueASCII(switches::kNetLogCaptureMode); @@ -173,7 +177,10 @@ void NetworkModule::Initialize(const std::string& user_agent_string, capture_mode = net::NetLogCaptureMode::IncludeSocketBytes(); } } - net_log_.reset(new CobaltNetLog(net_log_path, capture_mode)); + net_log_.reset(new CobaltNetLog(net_log_path_, capture_mode)); + net_log_->StartObserving(); + } else { + net_log_.reset(new CobaltNetLog(net_log_path_, capture_mode)); } #endif @@ -244,5 +251,20 @@ void NetworkModule::AddClientHintHeaders( } } +void NetworkModule::StartNetLog() { +#if defined(ENABLE_NETWORK_LOGGING) + LOG(INFO) << "Starting NetLog capture"; + net_log_->StartObserving(); +#endif +} + +base::FilePath NetworkModule::StopNetLog() { +#if defined(ENABLE_NETWORK_LOGGING) + LOG(INFO) << "Stopping NetLog capture"; + net_log_->StopObserving(); +#endif + return net_log_path_; +} + } // namespace network } // namespace cobalt diff --git a/cobalt/network/network_module.h b/cobalt/network/network_module.h index b6c8bd27468a..27e637a1852a 100644 --- a/cobalt/network/network_module.h +++ b/cobalt/network/network_module.h @@ -142,6 +142,11 @@ class NetworkModule : public base::MessageLoop::DestructionObserver { // From base::MessageLoop::DestructionObserver. void WillDestroyCurrentMessageLoop() override; + // Used to capture NetLog from Devtools + void StartNetLog(); + base::FilePath StopNetLog(); + + private: void Initialize(const std::string& user_agent_string, base::EventDispatcher* event_dispatcher); @@ -163,7 +168,9 @@ class NetworkModule : public base::MessageLoop::DestructionObserver { scoped_refptr dial_service_proxy_; #endif std::unique_ptr net_poster_; - std::unique_ptr net_log_; + + base::FilePath net_log_path_; + std::unique_ptr net_log_{nullptr}; Options options_; DISALLOW_COPY_AND_ASSIGN(NetworkModule); diff --git a/third_party/devtools/front_end/cobalt/cobalt.js b/third_party/devtools/front_end/cobalt/cobalt.js index 40fb60ae4148..444d9fe6f360 100644 --- a/third_party/devtools/front_end/cobalt/cobalt.js +++ b/third_party/devtools/front_end/cobalt/cobalt.js @@ -58,6 +58,32 @@ export default class CobaltPanel extends UI.VBox { }); })); }); + + const netLogContainer = this.element.createChild('div', 'netlog-container'); + netLogContainer.appendChild(UI.createTextButton(Common.UIString('Start NetLog'), event => { + console.log("Start NetLog"); + this.run(`(function() { window.h5vcc.netLog.start();})()`); + console.log("Started NetLog"); + })); + netLogContainer.appendChild(UI.createTextButton(Common.UIString('Stop NetLog'), event => { + console.log("Stop NetLog"); + this.run(`(function() { window.h5vcc.netLog.stop();})()`); + console.log("Stopped NetLog"); + })); + netLogContainer.appendChild(UI.createTextButton(Common.UIString('Download NetLog'), event => { + console.log("Download Trace"); + this.run(`(function() { return window.h5vcc.netLog.stopAndRead();})()`).then(function (result) { + const netlog_file = 'net_log.json'; + download_element.setAttribute('href', 'data:text/plain;charset=utf-8,' + + encodeURIComponent(result.result.value)); + download_element.setAttribute('download', netlog_file); + console.log("Downloaded NetLog"); + download_element.click(); + download_element.setAttribute('href', undefined); + }); + })); + + const debugLogContainer = this.element.createChild('div', 'debug-log-container'); debugLogContainer.appendChild(UI.createTextButton(Common.UIString('DebugLog On'), event => { this._cobaltAgent.invoke_sendConsoleCommand({ @@ -69,6 +95,7 @@ export default class CobaltPanel extends UI.VBox { command: 'debug_log', message: 'off' }); })); + const lifecycleContainer = this.element.createChild('div', 'lifecycle-container'); lifecycleContainer.appendChild(UI.createTextButton(Common.UIString('Blur'), event => { this._cobaltAgent.invoke_sendConsoleCommand({ command: 'blur' }); @@ -88,6 +115,7 @@ export default class CobaltPanel extends UI.VBox { lifecycleContainer.appendChild(UI.createTextButton(Common.UIString('Quit'), event => { this._cobaltAgent.invoke_sendConsoleCommand({ command: 'quit' }); })); + const consoleContainer = this.element.createChild('div', 'console-container'); consoleContainer.appendChild(UI.createTextButton(Common.UIString('DebugCommand'), event => { const outputElement = document.getElementsByClassName('console-output')[0]; From cc5d09bec82e73bcfa1b615901a2a3c2d83a6025 Mon Sep 17 00:00:00 2001 From: Kaido Kert Date: Mon, 23 Oct 2023 14:53:27 -0700 Subject: [PATCH 113/140] Update LTS minor version to 14 (#1833) b/260110906 --- cobalt/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/version.h b/cobalt/version.h index 9911a87f4040..2620123cedde 100644 --- a/cobalt/version.h +++ b/cobalt/version.h @@ -35,6 +35,6 @@ // release is cut. //. -#define COBALT_VERSION "24.lts.13" +#define COBALT_VERSION "24.lts.14" #endif // COBALT_VERSION_H_ From d23d4d3e860274e4cd4b11f5368f063236702d65 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 24 Oct 2023 18:06:47 -0700 Subject: [PATCH 114/140] Cherry pick PR #1839: Perma-launch Client Hint Headers (#1842) Refer to the original PR: https://github.com/youtube/cobalt/pull/1839 Remove dependency on persistent settings to control the addition of headers. b/285656784 Co-authored-by: Garo Bournoutian --- cobalt/h5vcc/h5vcc_settings.cc | 17 ----------------- cobalt/network/network_module.cc | 16 +--------------- cobalt/network/network_module.h | 13 ++++--------- 3 files changed, 5 insertions(+), 41 deletions(-) diff --git a/cobalt/h5vcc/h5vcc_settings.cc b/cobalt/h5vcc/h5vcc_settings.cc index 69aabbf248ac..4c47039c28d5 100644 --- a/cobalt/h5vcc/h5vcc_settings.cc +++ b/cobalt/h5vcc/h5vcc_settings.cc @@ -50,7 +50,6 @@ bool H5vccSettings::Set(const std::string& name, SetValueType value) const { const char kMediaPrefix[] = "Media."; const char kMediaCodecBlockList[] = "MediaCodecBlockList"; const char kNavigatorUAData[] = "NavigatorUAData"; - const char kClientHintHeaders[] = "ClientHintHeaders"; const char kQUIC[] = "QUIC"; #if SB_IS(EVERGREEN) @@ -81,22 +80,6 @@ bool H5vccSettings::Set(const std::string& name, SetValueType value) const { return true; } - if (name.compare(kClientHintHeaders) == 0 && value.IsType()) { - if (!persistent_settings_) { - return false; - } else { - persistent_settings_->SetPersistentSetting( - network::kClientHintHeadersEnabledPersistentSettingsKey, - std::make_unique(value.AsType())); - // Tell NetworkModule (if exists) to re-query persistent settings. - if (network_module_) { - network_module_ - ->SetEnableClientHintHeadersFlagsFromPersistentSettings(); - } - return true; - } - } - if (name.compare(kQUIC) == 0 && value.IsType()) { if (!persistent_settings_) { return false; diff --git a/cobalt/network/network_module.cc b/cobalt/network/network_module.cc index 44cce0d66409..0c8f67e6b911 100644 --- a/cobalt/network/network_module.cc +++ b/cobalt/network/network_module.cc @@ -113,18 +113,6 @@ void NetworkModule::SetEnableQuicFromPersistentSettings() { } } -void NetworkModule::SetEnableClientHintHeadersFlagsFromPersistentSettings() { - // Called on initialization and when the persistent setting is changed. - // If persistent setting is not set, will default to - // kCallTypeLoader | kCallTypeXHR. - if (options_.persistent_settings != nullptr) { - enable_client_hint_headers_flags_.store( - options_.persistent_settings->GetPersistentSettingAsInt( - kClientHintHeadersEnabledPersistentSettingsKey, - (kCallTypeLoader | kCallTypeXHR))); - } -} - void NetworkModule::EnsureStorageManagerStarted() { DCHECK(storage_manager_); storage_manager_->EnsureStarted(); @@ -143,8 +131,6 @@ void NetworkModule::Initialize(const std::string& user_agent_string, http_user_agent_settings_.reset(new net::StaticHttpUserAgentSettings( options_.preferred_language, user_agent_string)); - SetEnableClientHintHeadersFlagsFromPersistentSettings(); - #if defined(ENABLE_DEBUG_COMMAND_LINE_SWITCHES) base::CommandLine* command_line = base::CommandLine::ForCurrentProcess(); @@ -244,7 +230,7 @@ void NetworkModule::OnCreate(base::WaitableEvent* creation_event) { void NetworkModule::AddClientHintHeaders( net::URLFetcher& url_fetcher, ClientHintHeadersCallType call_type) const { - if (enable_client_hint_headers_flags_.load() & call_type) { + if (kEnabledClientHintHeaders & call_type) { for (const auto& header : client_hint_headers_) { url_fetcher.AddExtraRequestHeader(header); } diff --git a/cobalt/network/network_module.h b/cobalt/network/network_module.h index 27e637a1852a..18ea05c0f6df 100644 --- a/cobalt/network/network_module.h +++ b/cobalt/network/network_module.h @@ -49,7 +49,7 @@ namespace cobalt { namespace network { // Used to differentiate type of network call for Client Hint Headers. -// Values correspond to bit masks against |enable_client_hint_headers_flags_|. +// Values correspond to bit masks against |kEnabledClientHintHeaders|. enum ClientHintHeadersCallType : int32_t { kCallTypeLoader = (1u << 0), kCallTypeMedia = (1u << 1), @@ -59,11 +59,10 @@ enum ClientHintHeadersCallType : int32_t { kCallTypeXHR = (1u << 5), }; -const char kQuicEnabledPersistentSettingsKey[] = "QUICEnabled"; +// Determines which type of network calls should include Client Hint Headers. +constexpr int32_t kEnabledClientHintHeaders = (kCallTypeLoader | kCallTypeXHR); -// Holds bit mask flag, read into |enable_client_hint_headers_flags_|. -const char kClientHintHeadersEnabledPersistentSettingsKey[] = - "clientHintHeadersEnabled"; +const char kQuicEnabledPersistentSettingsKey[] = "QUICEnabled"; class NetworkSystem; // NetworkModule wraps various networking-related components such as @@ -132,9 +131,6 @@ class NetworkModule : public base::MessageLoop::DestructionObserver { void SetEnableQuicFromPersistentSettings(); - // Checks persistent settings to determine if Client Hint Headers are enabled. - void SetEnableClientHintHeadersFlagsFromPersistentSettings(); - // Adds the Client Hint Headers to the provided URLFetcher if enabled. void AddClientHintHeaders(net::URLFetcher& url_fetcher, ClientHintHeadersCallType call_type) const; @@ -154,7 +150,6 @@ class NetworkModule : public base::MessageLoop::DestructionObserver { std::unique_ptr CreateNetPoster(); std::vector client_hint_headers_; - starboard::atomic_int32_t enable_client_hint_headers_flags_; std::unique_ptr storage_manager_; std::unique_ptr thread_; std::unique_ptr url_request_context_; From a191c54849a709b72ca6820f021bc37f0e9d303e Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 25 Oct 2023 09:45:14 -0700 Subject: [PATCH 115/140] Cherry pick PR #1836: Starboardize internal Timer class in GTEST (#1840) Refer to the original PR: https://github.com/youtube/cobalt/pull/1836 b/280671902 Change-Id: Ic4f7f5f61c69c45be71529c567a6324122c6471b Co-authored-by: Arjun Menon --- .../googletest/src/googletest/src/gtest.cc | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/third_party/googletest/src/googletest/src/gtest.cc b/third_party/googletest/src/googletest/src/gtest.cc index 9b84415357df..2465a488de53 100644 --- a/third_party/googletest/src/googletest/src/gtest.cc +++ b/third_party/googletest/src/googletest/src/gtest.cc @@ -1132,6 +1132,21 @@ std::string UnitTestImpl::CurrentOsStackTraceExceptTop(int skip_count) { } // A helper class for measuring elapsed times. +#if GTEST_OS_STARBOARD +class Timer { + public: + Timer() : start_(GetTimeInMillis()) { + } + + // Return time elapsed in milliseconds since the timer was created. + TimeInMillis Elapsed() { + return (GetTimeInMillis() - start_); + } + + private: + TimeInMillis start_; +}; +#else // GTEST_OS_STARBOARD class Timer { public: Timer() : start_(std::chrono::steady_clock::now()) {} @@ -1146,6 +1161,7 @@ class Timer { private: std::chrono::steady_clock::time_point start_; }; +#endif // GTEST_OS_STARBOARD // Returns a timestamp as milliseconds since the epoch. Note this time may jump // around subject to adjustments by the system, to measure elapsed time use From 39047c955d30d764e63ec648018d0946ca3df28d Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 25 Oct 2023 10:45:38 -0700 Subject: [PATCH 116/140] Cherry pick PR #1834: [XB1] Add required arg to function call (#1845) Refer to the original PR: https://github.com/youtube/cobalt/pull/1834 b/306674453 Change-Id: I63826cf662089a58a29387e697a51c58fd5bf1f0 Co-authored-by: Tyler Holcombe --- starboard/xb1/tools/xb1_launcher.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/starboard/xb1/tools/xb1_launcher.py b/starboard/xb1/tools/xb1_launcher.py index 93ab0f1af0fe..7f9a74baa886 100644 --- a/starboard/xb1/tools/xb1_launcher.py +++ b/starboard/xb1/tools/xb1_launcher.py @@ -412,7 +412,7 @@ def UninstallSubPackages(self): package_full_name = package['PackageFullName'] if package_full_name.find( _DEFAULT_PACKAGE_NAME) != -1 or package_full_name.find( - _STUB_PACKAGE_NAME): + _STUB_PACKAGE_NAME) != -1: if package_full_name not in uninstalled_packages: self._LogLn('Existing YouTube app found on device. Uninstalling: ' + package_full_name) @@ -425,7 +425,7 @@ def UninstallSubPackages(self): self._LogLn(err.output) def DeleteLooseApps(self): - self._network_api.ClearLooseAppsFiles() + self._network_api.ClearLooseAppFiles() def Deploy(self): # starboard_arguments.txt is packaged with the appx. It instructs the app @@ -466,7 +466,7 @@ def Deploy(self): # Validate that app was installed correctly by checking to make sure # that the full package name can now be found. - def CheckPackageIsDeployed(self, package_name): + def CheckPackageIsDeployed(self, package_name=_DEFAULT_PACKAGE_NAME): package_list = self.WinAppDeployCmd('list') package_index = package_list.find(package_name) if package_index == -1: From 2430367412fe060ea7f7e5782baa6f98a8cc228b Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:18:46 -0700 Subject: [PATCH 117/140] Cherry pick PR #1745: Increase SbMediaConfigurationTest perf requirement (#1848) Refer to the original PR: https://github.com/youtube/cobalt/pull/1745 b/284140486 Co-authored-by: Jason --- starboard/nplb/media_configuration_test.cc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/starboard/nplb/media_configuration_test.cc b/starboard/nplb/media_configuration_test.cc index 28f0dbcc652f..432b12a9255e 100644 --- a/starboard/nplb/media_configuration_test.cc +++ b/starboard/nplb/media_configuration_test.cc @@ -28,9 +28,7 @@ TEST(SbMediaConfigurationTest, ValidatePerformance) { const int count_audio_output = SbMediaGetAudioOutputCount(); for (int i = 0; i < count_audio_output; ++i) { constexpr int kNumberOfCalls = 100; - // TODO(b/284140486): Optimize SbMediaGetAudioConfiguration() to reduce the - // time it takes to less than 0.5 milliseconds. - constexpr SbTime kMaxAverageTimePerCall = 3 * kSbTimeMillisecond; + constexpr SbTime kMaxAverageTimePerCall = 500; SbMediaAudioConfiguration configuration; TEST_PERF_FUNCWITHARGS_EXPLICIT(kNumberOfCalls, kMaxAverageTimePerCall, From 287697cf3e823fcaf1eea4c0665fb562c9b0cced Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:26:24 -0700 Subject: [PATCH 118/140] Cherry pick PR #1837: [android] Improve multi-encrypted-video demo (#1850) Refer to the original PR: https://github.com/youtube/cobalt/pull/1837 In Cobalt: * Removed tunnel mode check on audio decoder in Cobalt. The check prevents tunnel mode from being enabled via mime attribute, which is required to enable tunnel mode on the primary video of the demo, as mentioned below. The previous implementation checks whether the audio decoder supports tunnel mode before enabling it. However, none of them does, and the audio will be decoded to PCM before sending to the AudioTrack, so whether the audio decoder supports tunnel mode is irrelevant. In the demo: 1. Now the demo tries to play the primary video using tunnel mode, by passing "tunnelmode=true" as an extra mime attribute when supported. 2. Added more streams with different codecs and resolutions, and allow specifying them at runtime. 3. Try to create com.widevine.alpha when creation of com.youtube.widevine.l3 fails. This allows the demo to run on browsers without Widevine L3 support, including Chrome. 4. Download media data at startup to reduce CPU usage durnig playback. b/175883701 Change-Id: Id1e6c3f19a509f20b8f6413840b5ad223e5437ee --------- Co-authored-by: xiaomings --- .../multi-encrypted-video.html | 30 +- .../multi-encrypted-video.js | 274 +++++++++++++----- .../java/dev/cobalt/media/MediaCodecUtil.java | 11 +- .../shared/media_capabilities_cache.cc | 17 +- .../android/shared/media_capabilities_cache.h | 8 +- .../android/shared/media_codec_bridge.cc | 3 +- .../shared/media_is_audio_supported.cc | 19 +- .../shared/player_components_factory.h | 7 +- 8 files changed, 232 insertions(+), 137 deletions(-) diff --git a/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.html b/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.html index 36e504f9abba..8e3297a70d71 100644 --- a/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.html +++ b/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.html @@ -23,7 +23,7 @@ margin: 0; } - #player-layer { + #primary-player-layer { width: 100%; height: 100%; } @@ -33,13 +33,14 @@ height: 100%; } - #ui-layer { + #secondary-player-layer { position: absolute; - top: 15%; - height: 85%; + top: 60%; + height: 40%; width: 100%; - background-color: rgba(33, 33, 33, .75); padding: 24px; + display: flex; + justify-content: center; } .item { @@ -48,22 +49,23 @@ display: inline-block; margin: 24px; vertical-align: middle; + background-color: rgba(33, 33, 33, .75); } -

- +
+
-
-
- +
+
+
-
- +
+
-
- +
+
diff --git a/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.js b/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.js index 028d11c1a552..27dcddf9f948 100644 --- a/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.js +++ b/cobalt/demos/content/multi-encrypted-video/multi-encrypted-video.js @@ -12,6 +12,58 @@ // See the License for the specific language governing permissions and // limitations under the License. + +// Dictionary mapping string descriptions to media file descriptions in the +// form of [contentType, url, maxVideoCapabilities (for videos only), licenseUrl] +const MEDIA_FILES = { + 'av1_720p_60fps_drm': { + contentType: 'video/mp4; codecs="av01.0.05M.08"', + url: 'https://storage.googleapis.com/ytlr-cert.appspot.com/test-materials/media/av1-senc/sdr_720p60.mp4', + maxVideoCapabilities: 'width=1280; height=720', + licenseUrl: 'https://dash-mse-test.appspot.com/api/drm/widevine?drm_system=widevine&source=YOUTUBE&ip=0.0.0.0&ipbits=0&expire=19000000000&key=ik0&sparams=ip,ipbits,expire,drm_system,source,video_id&video_id=6508f99557a8385f&signature=5153900DAC410803EC269D252DAAA82BA6D8B825.495E631E406584A8EFCB4E9C9F3D45F6488B94E4', + }, + // 40 MB + 'h264_720p_24fps_drm': { + contentType: 'video/mp4; codecs="avc1.640028"', + url: 'http://yt-dash-mse-test.commondatastorage.googleapis.com/media/oops_cenc-20121114-145-no-clear-start.mp4', + maxVideoCapabilities: 'width=1280; height=720', + licenseUrl: 'https://dash-mse-test.appspot.com/api/drm/widevine?drm_system=widevine&source=YOUTUBE&ip=0.0.0.0&ipbits=0&expire=19000000000&key=test_key1&sparams=ip,ipbits,expire,drm_system,source,video_id&video_id=03681262dc412c06&signature=9C4BE99E6F517B51FED1F0B3B31966D3C5DAB9D6.6A1F30BB35F3A39A4CA814B731450D4CBD198FFD', + }, + // 38 MB + 'h264_720p_60fps_drm': { + contentType: 'video/mp4; codecs="avc1.640028"', + url: 'https://storage.googleapis.com/ytlr-cert.appspot.com/test-materials/media/drml3NoHdcp_h264_720p_60fps_cenc.mp4', + maxVideoCapabilities: 'width=1280; height=720', + licenseUrl: 'https://dash-mse-test.appspot.com/api/drm/widevine?drm_system=widevine&source=YOUTUBE&ip=0.0.0.0&ipbits=0&expire=19000000000&key=test_key1&sparams=ip,ipbits,expire,drm_system,source,video_id&video_id=03681262dc412c06&signature=9C4BE99E6F517B51FED1F0B3B31966D3C5DAB9D6.6A1F30BB35F3A39A4CA814B731450D4CBD198FFD', + }, + // 32 MB + 'vp9_720p_60fps_drm': { + contentType: 'video/webm; codecs="vp9"', + url: 'https://storage.googleapis.com/ytlr-cert.appspot.com/test-materials/media/drml3NoHdcp_vp9_720p_60fps_enc.webm', + maxVideoCapabilities: 'width=1280; height=720', + licenseUrl: 'https://dash-mse-test.appspot.com/api/drm/widevine?drm_system=widevine&source=YOUTUBE&ip=0.0.0.0&ipbits=0&expire=19000000000&key=test_key1&sparams=ip,ipbits,expire,drm_system,source,video_id&video_id=f320151fa3f061b2&signature=81E7B33929F9F35922F7D2E96A5E7AC36F3218B2.673F553EE51A48438AE5E707AEC87A071B4FEF65' + }, + // 1 MB + // Mono won't work with tunnel mode on many devices. + 'aac_mono_drm': { + contentType: 'audio/mp4; codecs="mp4a.40.2"', + url: 'http://yt-dash-mse-test.commondatastorage.googleapis.com/media/oops_cenc-20121114-148.mp4', + licenseUrl: 'https://dash-mse-test.appspot.com/api/drm/widevine?drm_system=widevine&source=YOUTUBE&ip=0.0.0.0&ipbits=0&expire=19000000000&key=test_key1&sparams=ip,ipbits,expire,drm_system,source,video_id&video_id=03681262dc412c06&signature=9C4BE99E6F517B51FED1F0B3B31966D3C5DAB9D6.6A1F30BB35F3A39A4CA814B731450D4CBD198FFD', + }, + // 2.8 MB + 'aac_clear': { + contentType: 'audio/mp4; codecs="mp4a.40.2"', + url: 'http://yt-dash-mse-test.commondatastorage.googleapis.com/media/car-20120827-8c.mp4', + }, + // 1.7 MB + 'opus_clear': { + contentType: 'audio/webm; codecs="opus"', + url: 'https://storage.googleapis.com/ytlr-cert.appspot.com/test-materials/media/car_opus_med.webm', + }, +}; + +mediaCache = {} + function fetchArrayBuffer(method, url, body, callback) { var xhr = new XMLHttpRequest(); xhr.responseType = 'arraybuffer'; @@ -22,6 +74,16 @@ function fetchArrayBuffer(method, url, body, callback) { xhr.send(body); } +async function fetchMediaData(mediaFileId) { + if (mediaFileId in mediaCache) { + return mediaCache[mediaFileId]; + } + + const response = await fetch(MEDIA_FILES[mediaFileId].url); + mediaCache[mediaFileId] = await response.arrayBuffer(); + return mediaCache[mediaFileId]; +} + function extractLicense(licenseArrayBuffer) { var licenseArray = new Uint8Array(licenseArrayBuffer); var licenseStartIndex = licenseArray.length - 2; @@ -37,80 +99,156 @@ function extractLicense(licenseArrayBuffer) { return licenseArray.subarray(licenseStartIndex); } -var videoContentType = 'video/mp4; codecs="avc1.640028"'; -var audioContentType = 'audio/mp4; codecs="mp4a.40.2"'; - -function play(videoElementId, keySystem) { - navigator.requestMediaKeySystemAccess(keySystem, [{ - 'initDataTypes': ['cenc'], - 'videoCapabilities': [{'contentType': videoContentType}], - 'audioCapabilities': [{'contentType': audioContentType}] - }]).then(function(mediaKeySystemAccess) { - return mediaKeySystemAccess.createMediaKeys(); - }).then(function(mediaKeys) { - var videoElement = document.getElementById(videoElementId); - - if (videoElementId != 'primary-video') { - videoElement.setMaxVideoCapabilities('width=1280; height=720'); +async function createMediaKeySystem(isPrimaryVideo, audioContentType, videoContentType) { + const keySystems = isPrimaryVideo ? ['com.widevine.alpha'] : ['com.youtube.widevine.l3', 'com.widevine.alpha']; + for (keySystem of keySystems) { + try { + mediaKeySystemAccess = await navigator.requestMediaKeySystemAccess(keySystem, [{ + 'initDataTypes': ['cenc', 'webm'], + 'audioCapabilities': [{'contentType': audioContentType}], + 'videoCapabilities': [{'contentType': videoContentType}]}]); + return mediaKeySystemAccess.createMediaKeys(); + } catch { + console.log('create keySystem ' + keySystem + ' failed.') + continue; } + } +} - if (mediaKeys.getMetrics) { - console.log('Found getMetrics(), calling it ...'); - try { - mediaKeys.getMetrics(); - console.log('Calling getMetrics() succeeded.'); - } catch(e) { - console.log('Calling getMetrics() failed.'); - } - } +function createTunnelModeContentType(videoContentType, tunnelModeAttributeValue) { + return videoContentType + '; tunnelmode=' + tunnelModeAttributeValue; +} - videoElement.setMediaKeys(mediaKeys); - - mediaKeySession = mediaKeys.createSession(); - mediaKeySession.addEventListener('message', function(messageEvent) { - var licenseServerUrl = 'https://dash-mse-test.appspot.com/api/drm/widevine?drm_system=widevine&source=YOUTUBE&ip=0.0.0.0&ipbits=0&expire=19000000000&key=test_key1&sparams=ip,ipbits,expire,drm_system,source,video_id&video_id=03681262dc412c06&signature=9C4BE99E6F517B51FED1F0B3B31966D3C5DAB9D6.6A1F30BB35F3A39A4CA814B731450D4CBD198FFD'; - fetchArrayBuffer('POST', licenseServerUrl, messageEvent.message, - function(licenseArrayBuffer) { - mediaKeySession.update(extractLicense(licenseArrayBuffer)); - }); - }); - - videoElement.addEventListener('encrypted', function(encryptedEvent) { - mediaKeySession.generateRequest( - encryptedEvent.initDataType, encryptedEvent.initData); - }); - - var mediaSource = new MediaSource(); - mediaSource.addEventListener('sourceopen', function() { - var videoSourceBuffer = mediaSource.addSourceBuffer(videoContentType); - fetchArrayBuffer('GET', - 'http://yt-dash-mse-test.commondatastorage.googleapis.com/media/oops_cenc-20121114-145-no-clear-start.mp4', - null, - function(videoArrayBuffer) { - videoSourceBuffer.appendBuffer(videoArrayBuffer); - }); +function isTunnelModeSupported(videoContentType) { + if (!MediaSource.isTypeSupported(videoContentType)) { + // If the content type isn't supported at all, it won't be supported in + // tunnel mode. + return false; + } + if (MediaSource.isTypeSupported(createTunnelModeContentType(videoContentType, 'invalid'))) { + // The implementation doesn't understand the `tunnelmode` attribute. + return false; + } + return MediaSource.isTypeSupported(createTunnelModeContentType(videoContentType, 'true')); +} + +async function play(videoElementId, videoFileId, optionalAudioFileId) { + const isPrimaryVideo = videoElementId == 'primary-video'; + + videoContentType = MEDIA_FILES[videoFileId].contentType; + if (isTunnelModeSupported(videoContentType)) { + videoContentType = createTunnelModeContentType(videoContentType, 'true'); + } + + var mediaKeys = await createMediaKeySystem(isPrimaryVideo, optionalAudioFileId ? MEDIA_FILES[optionalAudioFileId].contentType : MEDIA_FILES['opus_clear'].contentType, videoContentType); + var videoElement = document.getElementById(videoElementId); + + if (!isPrimaryVideo && videoElement.setMaxVideoCapabilities) { + videoElement.setMaxVideoCapabilities(MEDIA_FILES[videoFileId].maxVideoCapabilities); + } - var audioSourceBuffer = mediaSource.addSourceBuffer(audioContentType); - fetchArrayBuffer('GET', - 'http://yt-dash-mse-test.commondatastorage.googleapis.com/media/oops_cenc-20121114-148.mp4', - null, - function(audioArrayBuffer) { - audioSourceBuffer.appendBuffer(audioArrayBuffer); + videoElement.setMediaKeys(mediaKeys); + + mediaKeySession = mediaKeys.createSession(); + var licenseServerUrl = MEDIA_FILES[videoFileId].licenseUrl; + mediaKeySession.addEventListener('message', function(messageEvent) { + fetchArrayBuffer('POST', licenseServerUrl, messageEvent.message, + function(licenseArrayBuffer) { + mediaKeySession.update(extractLicense(licenseArrayBuffer)); }); - }); + }); + + videoElement.addEventListener('encrypted', function(encryptedEvent) { + mediaKeySession.generateRequest( + encryptedEvent.initDataType, encryptedEvent.initData); + }); + + var mediaSource = new MediaSource(); + mediaSource.addEventListener('sourceopen', async function() { + var videoSourceBuffer = mediaSource.addSourceBuffer(videoContentType); + var audioSourceBuffer; + + if (optionalAudioFileId) { + audioSourceBuffer = mediaSource.addSourceBuffer(MEDIA_FILES[optionalAudioFileId].contentType); + } + + var videoArrayBuffer = await fetchMediaData(videoFileId); + videoSourceBuffer.appendBuffer(videoArrayBuffer); - videoElement.src = URL.createObjectURL(mediaSource); - videoElement.play(); + if (audioSourceBuffer) { + var audioArrayBuffer = await fetchMediaData(optionalAudioFileId); + audioSourceBuffer.appendBuffer(audioArrayBuffer); + } }); + + videoElement.src = URL.createObjectURL(mediaSource); + videoElement.play(); +} + +function getGetParameters() { + var parsedParameters = {}; + + const urlComponents = window.location.href.split('?'); + if (urlComponents.length < 2) { + return parsedParameters; + } + + const query = urlComponents[1]; + const parameters = query.split('&'); + + for (parameter of parameters) { + const split = parameter.split('='); + if (split.length == 0) { + continue; + } + if (split.length == 1) { + parsedParameters[split[0]] = ''; + } else { + parsedParameters[split[0]] = split[1]; + } + } + + return parsedParameters; } -play('primary-video', 'com.widevine.alpha'); -window.setTimeout(function() { - play('secondary-video-1', 'com.youtube.widevine.l3'); -}, 10000); -window.setTimeout(function() { - play('secondary-video-2', 'com.youtube.widevine.l3'); -}, 20000); -window.setTimeout(function() { - play('secondary-video-3', 'com.youtube.widevine.l3'); -}, 30000); +function populateMediaFileIds() { + var mediaFileIds = []; + const getParameters = getGetParameters(); + + mediaFileIds['video0'] = getParameters['video0'] ?? 'vp9_720p_60fps_drm'; + mediaFileIds['video1'] = getParameters['video1'] ?? 'h264_720p_24fps_drm'; + mediaFileIds['video2'] = getParameters['video2'] ?? 'vp9_720p_60fps_drm'; + mediaFileIds['video3'] = getParameters['video3'] ?? 'h264_720p_24fps_drm'; + mediaFileIds['audio'] = getParameters['audio'] ?? 'opus_clear'; + + return mediaFileIds; +} + +async function prefetchMediaData(mediaFileIds) { + for (mediaFileId of Object.keys(mediaFileIds)) { + await fetchMediaData(mediaFileIds[mediaFileId]); + } +} + +async function main() { + if (window.h5vcc && window.h5vcc.settings) { + h5vcc.settings.set('MediaSource.EnableAvoidCopyingArrayBuffer', 1); + } + + const mediaFileIds = populateMediaFileIds(); + await prefetchMediaData(mediaFileIds); + + play('primary-video', mediaFileIds['video0'], mediaFileIds['audio']); + window.setTimeout(function() { + play('secondary-video-1', mediaFileIds['video1']); + }, 10000); + window.setTimeout(function() { + play('secondary-video-2', mediaFileIds['video2']); + }, 20000); + window.setTimeout(function() { + play('secondary-video-3', mediaFileIds['video3']); + }, 30000); +} + + +main(); diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java index ebe9c2bd0e50..50610e06efcd 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaCodecUtil.java @@ -747,8 +747,7 @@ public static String findVideoDecoder( * "" otherwise. */ @UsedByNative - public static String findAudioDecoder( - String mimeType, int bitrate, boolean mustSupportTunnelMode) { + public static String findAudioDecoder(String mimeType, int bitrate) { // Note: MediaCodecList is sorted by the framework such that the best decoders come first. for (MediaCodecInfo info : new MediaCodecList(MediaCodecList.ALL_CODECS).getCodecInfos()) { if (info.isEncoder()) { @@ -766,14 +765,6 @@ public static String findAudioDecoder( if (!bitrateRange.contains(bitrate)) { continue; } - if (mustSupportTunnelMode - && !codecCapabilities.isFeatureSupported(CodecCapabilities.FEATURE_TunneledPlayback)) { - continue; - } - // TODO: Determine if we can safely check if an audio codec requires the tunneled playback - // feature. i.e., reject when |mustSupportTunnelMode| == false - // and codecCapabilities.isFeatureRequired(CodecCapabilities.FEATURE_TunneledPlayback) == - // true. return name; } } diff --git a/starboard/android/shared/media_capabilities_cache.cc b/starboard/android/shared/media_capabilities_cache.cc index 2d07a7b061dd..ed2674d08e86 100644 --- a/starboard/android/shared/media_capabilities_cache.cc +++ b/starboard/android/shared/media_capabilities_cache.cc @@ -472,10 +472,8 @@ bool MediaCapabilitiesCache::GetAudioConfiguration( } bool MediaCapabilitiesCache::HasAudioDecoderFor(const std::string& mime_type, - int bitrate, - bool must_support_tunnel_mode) { - return !FindAudioDecoder(mime_type, bitrate, must_support_tunnel_mode) - .empty(); + int bitrate) { + return !FindAudioDecoder(mime_type, bitrate).empty(); } bool MediaCapabilitiesCache::HasVideoDecoderFor( @@ -497,16 +495,14 @@ bool MediaCapabilitiesCache::HasVideoDecoderFor( std::string MediaCapabilitiesCache::FindAudioDecoder( const std::string& mime_type, - int bitrate, - bool must_support_tunnel_mode) { + int bitrate) { if (!is_enabled_) { JniEnvExt* env = JniEnvExt::Get(); ScopedLocalJavaRef j_mime( env->NewStringStandardUTFOrAbort(mime_type.c_str())); jobject j_decoder_name = env->CallStaticObjectMethodOrAbort( "dev/cobalt/media/MediaCodecUtil", "findAudioDecoder", - "(Ljava/lang/String;IZ)Ljava/lang/String;", j_mime.Get(), bitrate, - must_support_tunnel_mode); + "(Ljava/lang/String;I)Ljava/lang/String;", j_mime.Get(), bitrate); return env->GetStringStandardUTFOrAbort( static_cast(j_decoder_name)); } @@ -515,11 +511,6 @@ std::string MediaCapabilitiesCache::FindAudioDecoder( UpdateMediaCapabilities_Locked(); for (auto& audio_capability : audio_codec_capabilities_map_[mime_type]) { - // Reject if tunnel mode is required but codec doesn't support it. - if (must_support_tunnel_mode && - !audio_capability->is_tunnel_mode_supported()) { - continue; - } // Reject if bitrate is not supported. if (!audio_capability->IsBitrateSupported(bitrate)) { continue; diff --git a/starboard/android/shared/media_capabilities_cache.h b/starboard/android/shared/media_capabilities_cache.h index 0395d51683ca..cedfe99db720 100644 --- a/starboard/android/shared/media_capabilities_cache.h +++ b/starboard/android/shared/media_capabilities_cache.h @@ -128,9 +128,7 @@ class MediaCapabilitiesCache { bool GetAudioConfiguration(int index, SbMediaAudioConfiguration* configuration); - bool HasAudioDecoderFor(const std::string& mime_type, - int bitrate, - bool must_support_tunnel_mode); + bool HasAudioDecoderFor(const std::string& mime_type, int bitrate); bool HasVideoDecoderFor(const std::string& mime_type, bool must_support_secure, @@ -142,9 +140,7 @@ class MediaCapabilitiesCache { int bitrate, int fps); - std::string FindAudioDecoder(const std::string& mime_type, - int bitrate, - bool must_support_tunnel_mode); + std::string FindAudioDecoder(const std::string& mime_type, int bitrate); std::string FindVideoDecoder(const std::string& mime_type, bool must_support_secure, diff --git a/starboard/android/shared/media_codec_bridge.cc b/starboard/android/shared/media_codec_bridge.cc index fe970384ffd9..87bdf3286c07 100644 --- a/starboard/android/shared/media_codec_bridge.cc +++ b/starboard/android/shared/media_codec_bridge.cc @@ -169,8 +169,7 @@ scoped_ptr MediaCodecBridge::CreateAudioMediaCodecBridge( std::string decoder_name = MediaCapabilitiesCache::GetInstance()->FindAudioDecoder( - mime, /* bitrate = */ 0, - /* must_support_tunnel_mode = */ false); + mime, /* bitrate = */ 0); if (decoder_name.empty()) { SB_LOG(ERROR) << "Failed to find decoder for " << audio_stream_info.codec diff --git a/starboard/android/shared/media_is_audio_supported.cc b/starboard/android/shared/media_is_audio_supported.cc index 46b73e546fcc..9844406383db 100644 --- a/starboard/android/shared/media_is_audio_supported.cc +++ b/starboard/android/shared/media_is_audio_supported.cc @@ -40,7 +40,6 @@ bool SbMediaIsAudioSupported(SbMediaAudioCodec audio_codec, return false; } - bool enable_tunnel_mode = false; bool enable_audio_passthrough = true; if (mime_type) { if (!mime_type->is_valid()) { @@ -53,13 +52,6 @@ bool SbMediaIsAudioSupported(SbMediaAudioCodec audio_codec, return false; } - // Allows for enabling tunneled playback. Disabled by default. - // (https://source.android.com/devices/tv/multimedia-tunneling) - if (!mime_type->ValidateBoolParameter("tunnelmode")) { - return false; - } - enable_tunnel_mode = mime_type->GetParamBoolValue("tunnelmode", false); - // Enables audio passthrough if the codec supports it. if (!mime_type->ValidateBoolParameter("audiopassthrough")) { return false; @@ -75,14 +67,6 @@ bool SbMediaIsAudioSupported(SbMediaAudioCodec audio_codec, } } - if (enable_tunnel_mode && !SbAudioSinkIsAudioSampleTypeSupported( - kSbMediaAudioSampleTypeInt16Deprecated)) { - SB_LOG(WARNING) - << "Tunnel mode is rejected because int16 sample is required " - "but not supported."; - return false; - } - // Android uses a libopus based opus decoder for clear content, or a platform // opus decoder for encrypted content, if available. if (audio_codec == kSbMediaAudioCodecOpus) { @@ -90,8 +74,7 @@ bool SbMediaIsAudioSupported(SbMediaAudioCodec audio_codec, } bool media_codec_supported = - MediaCapabilitiesCache::GetInstance()->HasAudioDecoderFor( - mime, bitrate, enable_tunnel_mode); + MediaCapabilitiesCache::GetInstance()->HasAudioDecoderFor(mime, bitrate); if (!media_codec_supported) { return false; diff --git a/starboard/android/shared/player_components_factory.h b/starboard/android/shared/player_components_factory.h index 1530d5e96662..57d3b44d15b7 100644 --- a/starboard/android/shared/player_components_factory.h +++ b/starboard/android/shared/player_components_factory.h @@ -303,7 +303,6 @@ class PlayerComponentsFactory : public starboard::shared::starboard::player:: MimeType audio_mime_type(audio_mime); if (!audio_mime.empty()) { if (!audio_mime_type.is_valid() || - !audio_mime_type.ValidateBoolParameter("tunnelmode") || !audio_mime_type.ValidateBoolParameter("enableaudiodevicecallback") || !audio_mime_type.ValidateBoolParameter("enablepcmcontenttypemovie")) { *error_message = @@ -332,15 +331,11 @@ class PlayerComponentsFactory : public starboard::shared::starboard::player:: if (creation_parameters.audio_codec() != kSbMediaAudioCodecNone && creation_parameters.video_codec() != kSbMediaVideoCodecNone) { enable_tunnel_mode = - audio_mime_type.GetParamBoolValue("tunnelmode", false) && video_mime_type.GetParamBoolValue("tunnelmode", false); SB_LOG(INFO) << "Tunnel mode is " << (enable_tunnel_mode ? "enabled. " : "disabled. ") - << "Audio mime parameter \"tunnelmode\" value: " - << audio_mime_type.GetParamStringValue("tunnelmode", - "") - << ", video mime parameter \"tunnelmode\" value: " + << "Video mime parameter \"tunnelmode\" value: " << video_mime_type.GetParamStringValue("tunnelmode", "") << "."; From ed647c0aa06278f365cb5dd77885474290c71a7f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 25 Oct 2023 16:32:40 -0700 Subject: [PATCH 119/140] Cherry pick PR #1817: [UWP] Match time zone names with other platforms (#1849) Refer to the original PR: https://github.com/youtube/cobalt/pull/1817 Update time_zone_get_name to match how other platforms behave. b/304335954 Change-Id: I3862bcb6ae4118dad76b8a6687194e993927c539 Co-authored-by: Tyler Holcombe --- starboard/shared/uwp/time_zone_get_name.cc | 45 ++++++++++++++++++++ starboard/shared/win32/time_zone_get_name.cc | 4 ++ starboard/win/shared/BUILD.gn | 1 - starboard/win/win32/BUILD.gn | 1 + starboard/xb1/BUILD.gn | 1 + 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 starboard/shared/uwp/time_zone_get_name.cc diff --git a/starboard/shared/uwp/time_zone_get_name.cc b/starboard/shared/uwp/time_zone_get_name.cc new file mode 100644 index 000000000000..58fc8b974b11 --- /dev/null +++ b/starboard/shared/uwp/time_zone_get_name.cc @@ -0,0 +1,45 @@ +// Copyright 2023 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. + +#include "starboard/time_zone.h" + +#include +#include + +#include "starboard/once.h" +#include "starboard/shared/win32/wchar_utils.h" + +namespace { +class TimeZoneString { + public: + static TimeZoneString* Get(); + const char* value() const { return value_.c_str(); } + + private: + TimeZoneString() { + Windows::Globalization::Calendar ^ calendar = + ref new Windows::Globalization::Calendar(); + Platform::String ^ time_zone = calendar->GetTimeZone(); + value_ = starboard::shared::win32::platformStringToString(time_zone); + } + std::string value_; +}; + +SB_ONCE_INITIALIZE_FUNCTION(TimeZoneString, TimeZoneString::Get); +} // namespace. + +const char* SbTimeZoneGetName() { + const char* output = TimeZoneString::Get()->value(); + return output; +} diff --git a/starboard/shared/win32/time_zone_get_name.cc b/starboard/shared/win32/time_zone_get_name.cc index bc92e3b12d17..9c31522b65a9 100644 --- a/starboard/shared/win32/time_zone_get_name.cc +++ b/starboard/shared/win32/time_zone_get_name.cc @@ -28,6 +28,10 @@ class TimeZoneString { const char* value() const { return value_.c_str(); } private: + // Returns a string representing a time zone name, e.g. "EST" for Eastern + // Standard Time or "PDT" for Pacific Daylight Time. There isn't a native way + // to convert these to IANA name format on Windows without UWP, so we're + // making use of GetDynamicTimeZoneInformation for now. TimeZoneString() { DYNAMIC_TIME_ZONE_INFORMATION time_zone_info; DWORD zone_id = GetDynamicTimeZoneInformation(&time_zone_info); diff --git a/starboard/win/shared/BUILD.gn b/starboard/win/shared/BUILD.gn index 9501d8ff3334..2b0f501f8c01 100644 --- a/starboard/win/shared/BUILD.gn +++ b/starboard/win/shared/BUILD.gn @@ -304,7 +304,6 @@ static_library("starboard_platform") { "//starboard/shared/win32/time_get_now.cc", "//starboard/shared/win32/time_utils.h", "//starboard/shared/win32/time_zone_get_current.cc", - "//starboard/shared/win32/time_zone_get_name.cc", "//starboard/shared/win32/video_decoder.cc", "//starboard/shared/win32/video_decoder.h", "//starboard/shared/win32/wasapi_include.h", diff --git a/starboard/win/win32/BUILD.gn b/starboard/win/win32/BUILD.gn index e458622c25a3..746db846b634 100644 --- a/starboard/win/win32/BUILD.gn +++ b/starboard/win/win32/BUILD.gn @@ -59,6 +59,7 @@ static_library("starboard_platform") { "//starboard/shared/win32/system_get_used_cpu_memory.cc", "//starboard/shared/win32/system_raise_platform_error.cc", "//starboard/shared/win32/system_symbolize.cc", + "//starboard/shared/win32/time_zone_get_name.cc", "//starboard/shared/win32/window_create.cc", "//starboard/shared/win32/window_destroy.cc", "//starboard/shared/win32/window_get_platform_handle.cc", diff --git a/starboard/xb1/BUILD.gn b/starboard/xb1/BUILD.gn index 5a54c1669cb2..a0ad82bc0e2c 100644 --- a/starboard/xb1/BUILD.gn +++ b/starboard/xb1/BUILD.gn @@ -134,6 +134,7 @@ static_library("starboard_platform") { "//starboard/shared/uwp/system_platform_error_internal.cc", "//starboard/shared/uwp/system_platform_error_internal.h", "//starboard/shared/uwp/system_raise_platform_error.cc", + "//starboard/shared/uwp/time_zone_get_name.cc", "//starboard/shared/uwp/wasapi_audio.cc", "//starboard/shared/uwp/wasapi_audio.h", "//starboard/shared/uwp/wasapi_audio_sink.cc", From 0fa70786bd2f3e40e649621c3122c40374400ee4 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 26 Oct 2023 11:15:07 -0700 Subject: [PATCH 120/140] Cherry pick PR #1846: [android] Lazy load media capability to avoid ANR (#1852) Refer to the original PR: https://github.com/youtube/cobalt/pull/1846 1. Avoid any media capability queries from system callback, so if there's any display/audio device change, media capability cache would do a lazy reload. 2. Remove capability pre-loading to avoid querying capability right after app launch. 3. Reload all capabilities when there's any display device change to simplify the code. b/304602770 Co-authored-by: Jason --- .../java/dev/cobalt/coat/CobaltActivity.java | 4 -- .../shared/media_capabilities_cache.cc | 59 +++++-------------- .../android/shared/media_capabilities_cache.h | 12 ++-- 3 files changed, 18 insertions(+), 57 deletions(-) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java index 2a763280dd1d..129b3c1aca2f 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/CobaltActivity.java @@ -159,8 +159,6 @@ protected void onStart() { getStarboardBridge().onActivityStart(this, keyboardEditor); super.onStart(); - - nativeInitializeMediaCapabilitiesInBackground(); } @Override @@ -396,6 +394,4 @@ public void onLowMemory() { public long getAppStartTimestamp() { return timeInNanoseconds; } - - private static native void nativeInitializeMediaCapabilitiesInBackground(); } diff --git a/starboard/android/shared/media_capabilities_cache.cc b/starboard/android/shared/media_capabilities_cache.cc index ed2674d08e86..fe96cc8c96c8 100644 --- a/starboard/android/shared/media_capabilities_cache.cc +++ b/starboard/android/shared/media_capabilities_cache.cc @@ -604,24 +604,6 @@ std::string MediaCapabilitiesCache::FindVideoDecoder( return ""; } -void MediaCapabilitiesCache::ClearCache() { - ScopedLock scoped_lock(mutex_); - is_initialized_ = false; - supported_hdr_types_is_dirty_ = true; - is_widevine_supported_ = false; - is_cbcs_supported_ = false; - supported_transfer_ids_.clear(); - passthrough_supportabilities_.clear(); - audio_codec_capabilities_map_.clear(); - video_codec_capabilities_map_.clear(); - audio_configurations_.clear(); -} - -void MediaCapabilitiesCache::Initialize() { - ScopedLock scoped_lock(mutex_); - UpdateMediaCapabilities_Locked(); -} - MediaCapabilitiesCache::MediaCapabilitiesCache() { // Enable mime and key system caches. MimeSupportabilityCache::GetInstance()->SetCacheEnabled(true); @@ -630,19 +612,20 @@ MediaCapabilitiesCache::MediaCapabilitiesCache() { void MediaCapabilitiesCache::UpdateMediaCapabilities_Locked() { mutex_.DCheckAcquired(); - - if (supported_hdr_types_is_dirty_.exchange(false)) { + if (capabilities_is_dirty_.exchange(false)) { + // We use a different cache strategy (load and cache) for passthrough + // supportabilities, so we only clear |passthrough_supportabilities_| here. + passthrough_supportabilities_.clear(); + + audio_codec_capabilities_map_.clear(); + video_codec_capabilities_map_.clear(); + audio_configurations_.clear(); + is_widevine_supported_ = GetIsWidevineSupported(); + is_cbcs_supported_ = GetIsCbcsSupported(); supported_transfer_ids_ = GetSupportedHdrTypes(); + LoadCodecInfos_Locked(); + LoadAudioConfigurations_Locked(); } - - if (is_initialized_) { - return; - } - is_widevine_supported_ = GetIsWidevineSupported(); - is_cbcs_supported_ = GetIsCbcsSupported(); - LoadCodecInfos_Locked(); - LoadAudioConfigurations_Locked(); - is_initialized_ = true; } void MediaCapabilitiesCache::LoadCodecInfos_Locked() { @@ -713,33 +696,19 @@ void MediaCapabilitiesCache::LoadAudioConfigurations_Locked() { extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_util_DisplayUtil_nativeOnDisplayChanged() { - SB_DLOG(INFO) << "Display device has changed."; - MediaCapabilitiesCache::GetInstance()->ClearSupportedHdrTypes(); + // Display device change could change hdr capabilities. + MediaCapabilitiesCache::GetInstance()->ClearCache(); MimeSupportabilityCache::GetInstance()->ClearCachedMimeSupportabilities(); } extern "C" SB_EXPORT_PLATFORM void Java_dev_cobalt_media_AudioOutputManager_nativeOnAudioDeviceChanged() { - SB_DLOG(INFO) << "Audio device has changed."; // Audio output device change could change passthrough decoder capabilities, // so we have to reload codec capabilities. MediaCapabilitiesCache::GetInstance()->ClearCache(); MimeSupportabilityCache::GetInstance()->ClearCachedMimeSupportabilities(); } -void* MediaCapabilitiesCacheInitializationThreadEntry(void* context) { - SB_LOG(INFO) << "Initialize MediaCapabilitiesCache in background."; - MediaCapabilitiesCache::GetInstance()->Initialize(); - return 0; -} - -extern "C" SB_EXPORT_PLATFORM void -Java_dev_cobalt_coat_CobaltActivity_nativeInitializeMediaCapabilitiesInBackground() { - SbThreadCreate(0, kSbThreadPriorityNormal, kSbThreadNoAffinity, false, - "media_capabilities_cache_thread", - MediaCapabilitiesCacheInitializationThreadEntry, nullptr); -} - } // namespace shared } // namespace android } // namespace starboard diff --git a/starboard/android/shared/media_capabilities_cache.h b/starboard/android/shared/media_capabilities_cache.h index cedfe99db720..656398a30a9e 100644 --- a/starboard/android/shared/media_capabilities_cache.h +++ b/starboard/android/shared/media_capabilities_cache.h @@ -155,10 +155,7 @@ class MediaCapabilitiesCache { bool IsEnabled() const { return is_enabled_; } void SetCacheEnabled(bool enabled) { is_enabled_ = enabled; } - void ClearCache(); - - void Initialize(); - void ClearSupportedHdrTypes() { supported_hdr_types_is_dirty_ = true; } + void ClearCache() { capabilities_is_dirty_ = true; } private: MediaCapabilitiesCache(); @@ -184,12 +181,11 @@ class MediaCapabilitiesCache { std::map audio_codec_capabilities_map_; std::map video_codec_capabilities_map_; std::vector audio_configurations_; - - std::atomic_bool is_enabled_{true}; - std::atomic_bool supported_hdr_types_is_dirty_{true}; - bool is_initialized_ = false; bool is_widevine_supported_ = false; bool is_cbcs_supported_ = false; + + std::atomic_bool is_enabled_{true}; + std::atomic_bool capabilities_is_dirty_{true}; }; } // namespace shared From 607fe01d2e742537e870b5b5e338e634e88b832d Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Thu, 26 Oct 2023 13:35:13 -0700 Subject: [PATCH 121/140] Cherry pick PR #1776: [android] Handle error in MinRequiredFramesTester (#1851) Refer to the original PR: https://github.com/youtube/cobalt/pull/1776 b/175822670 Co-authored-by: Jason --- .../audio_sink_min_required_frames_tester.cc | 15 ++++++++++++--- .../audio_sink_min_required_frames_tester.h | 1 + 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/starboard/android/shared/audio_sink_min_required_frames_tester.cc b/starboard/android/shared/audio_sink_min_required_frames_tester.cc index b8139a284a42..1d156b9ae1f8 100644 --- a/starboard/android/shared/audio_sink_min_required_frames_tester.cc +++ b/starboard/android/shared/audio_sink_min_required_frames_tester.cc @@ -113,6 +113,7 @@ void MinRequiredFramesTester::TesterThreadFunc() { frame_buffers[0] = silence_buffer.data(); // Set default values. + has_error_ = false; min_required_frames_ = task.default_required_frames; total_consumed_frames_ = 0; last_underrun_count_ = -1; @@ -148,6 +149,13 @@ void MinRequiredFramesTester::TesterThreadFunc() { min_required_frames_ = max_required_frames_; } + if (has_error_) { + SB_LOG(ERROR) << "There's an error while running the test. Fallback to " + "max required frames " + << max_required_frames_ << "."; + min_required_frames_ = max_required_frames_; + } + if (start_threshold > min_required_frames_) { SB_LOG(INFO) << "Audio sink min required frames is overwritten from " << min_required_frames_ << " to audio track start threshold " @@ -197,9 +205,10 @@ void MinRequiredFramesTester::ErrorFunc(bool capability_changed, const std::string& error_message, void* context) { SB_LOG(ERROR) << "Error occurred while writing frames: " << error_message; - // TODO: Handle errors during minimum frames test, maybe by terminating the - // test earlier. - SB_NOTREACHED(); + + MinRequiredFramesTester* tester = + static_cast(context); + tester->has_error_ = true; } void MinRequiredFramesTester::UpdateSourceStatus(int* frames_in_buffer, diff --git a/starboard/android/shared/audio_sink_min_required_frames_tester.h b/starboard/android/shared/audio_sink_min_required_frames_tester.h index 669217aca4ec..934bdb977fb3 100644 --- a/starboard/android/shared/audio_sink_min_required_frames_tester.h +++ b/starboard/android/shared/audio_sink_min_required_frames_tester.h @@ -107,6 +107,7 @@ class MinRequiredFramesTester { std::vector test_tasks_; AudioTrackAudioSink* audio_sink_ = nullptr; int min_required_frames_; + std::atomic_bool has_error_; // Used only by audio sink thread. int total_consumed_frames_; From dbcca0503cb73d2e5e6b7a8c3669dddaeb253954 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 27 Oct 2023 15:53:31 -0700 Subject: [PATCH 122/140] Cherry pick PR #1858: Remove raspi 2 monolithic builders (#1866) Refer to the original PR: https://github.com/youtube/cobalt/pull/1858 Change default raspi builders to modular from monolithic Raspi- sb version13 , 14 monolithic builders can't be built modularly( as building modularly requires sb15 and above . These builders are removed from the build matrix. Note that sb version 13, 14 are being built by evergreen. Make raspi-2-skia build modularly. b/307977106 Co-authored-by: Niranjan Yardi --- .github/config/raspi-2-modular.json | 28 ---------------------- .github/config/raspi-2-skia.json | 2 +- .github/config/raspi-2.json | 24 ++----------------- .github/workflows/raspi-2_24.lts.1+.yaml | 9 ------- starboard/raspi/2/args.gn | 3 ++- starboard/raspi/2/skia/BUILD.gn | 6 +++++ starboard/raspi/2/skia/starboard_loader.cc | 19 +++++++++++++++ starboard/raspi/2/skia/toolchain/BUILD.gn | 21 ++++++++++++++++ starboard/raspi/shared/launcher.py | 8 +++---- starboard/raspi/shared/test_filters.py | 14 ----------- 10 files changed, 54 insertions(+), 80 deletions(-) delete mode 100644 .github/config/raspi-2-modular.json create mode 100644 starboard/raspi/2/skia/starboard_loader.cc diff --git a/.github/config/raspi-2-modular.json b/.github/config/raspi-2-modular.json deleted file mode 100644 index b9a40a772c89..000000000000 --- a/.github/config/raspi-2-modular.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "docker_service": "build-raspi", - "on_device_test": { - "enabled": true, - "tests": [ - "0", - "1", - "2", - "3", - "4", - "5" - ], - "test_attempts": 2 - }, - "platforms": [ - "raspi-2-modular" - ], - "includes": [ - { - "name":"modular", - "platform":"raspi-2-modular", - "target_platform":"raspi-2", - "target_cpu":"target_cpu=\\\"arm\\\"", - "extra_gn_arguments": "build_with_separate_cobalt_toolchain=true use_asan=false", - "dimension": "release_version=regex:10.*" - } - ] -} diff --git a/.github/config/raspi-2-skia.json b/.github/config/raspi-2-skia.json index f99fb737dcca..e83ea1fa8577 100644 --- a/.github/config/raspi-2-skia.json +++ b/.github/config/raspi-2-skia.json @@ -9,7 +9,7 @@ "platform":"raspi-2-skia", "target_platform":"raspi-2-skia", "target_cpu":"target_cpu=\\\"arm\\\"", - "extra_gn_arguments": "is_clang=false" + "extra_gn_arguments": "build_with_separate_cobalt_toolchain=true use_asan=false" } ] } diff --git a/.github/config/raspi-2.json b/.github/config/raspi-2.json index ab2a1e654865..9008437612cb 100644 --- a/.github/config/raspi-2.json +++ b/.github/config/raspi-2.json @@ -14,8 +14,6 @@ }, "platforms": [ "raspi-2", - "raspi-2-sbversion-13", - "raspi-2-sbversion-14", "raspi-2-sbversion-15" ], "includes": [ @@ -24,25 +22,7 @@ "platform":"raspi-2", "target_platform":"raspi-2", "target_cpu":"target_cpu=\\\"arm\\\"", - "extra_gn_arguments": "is_clang=false", - "dimension": "release_version=regex:10.*" - }, - { - "name":"sbversion-13", - "platform":"raspi-2-sbversion-13", - "target_platform":"raspi-2", - "target_cpu":"target_cpu=\\\"arm\\\"", - "extra_gn_arguments":"is_clang=false", - "sb_api_version": "sb_api_version=13", - "dimension": "release_version=regex:10.*" - }, - { - "name":"sbversion-14", - "platform":"raspi-2-sbversion-14", - "target_platform":"raspi-2", - "target_cpu":"target_cpu=\\\"arm\\\"", - "extra_gn_arguments":"is_clang=false", - "sb_api_version": "sb_api_version=14", + "extra_gn_arguments": "build_with_separate_cobalt_toolchain=true use_asan=false", "dimension": "release_version=regex:10.*" }, { @@ -50,7 +30,7 @@ "platform":"raspi-2-sbversion-15", "target_platform":"raspi-2", "target_cpu":"target_cpu=\\\"arm\\\"", - "extra_gn_arguments":"is_clang=false", + "extra_gn_arguments": "build_with_separate_cobalt_toolchain=true use_asan=false", "sb_api_version": "sb_api_version=15", "dimension": "release_version=regex:10.*" } diff --git a/.github/workflows/raspi-2_24.lts.1+.yaml b/.github/workflows/raspi-2_24.lts.1+.yaml index f6918b1ae853..9eb1ec0ac329 100644 --- a/.github/workflows/raspi-2_24.lts.1+.yaml +++ b/.github/workflows/raspi-2_24.lts.1+.yaml @@ -36,12 +36,3 @@ jobs: with: platform: raspi-2-skia nightly: ${{ github.event.inputs.nightly }} - raspi-2-modular: - uses: ./.github/workflows/main.yaml - permissions: - packages: write - pull-requests: write - with: - platform: raspi-2-modular - nightly: ${{ github.event.inputs.nightly }} - modular: true diff --git a/starboard/raspi/2/args.gn b/starboard/raspi/2/args.gn index 0b190f800347..9c2977749c07 100644 --- a/starboard/raspi/2/args.gn +++ b/starboard/raspi/2/args.gn @@ -15,4 +15,5 @@ target_platform = "raspi-2" target_os = "linux" target_cpu = "arm" -is_clang = false +build_with_separate_cobalt_toolchain = true +use_asan = false diff --git a/starboard/raspi/2/skia/BUILD.gn b/starboard/raspi/2/skia/BUILD.gn index bcb5b924184d..80f549ca1011 100644 --- a/starboard/raspi/2/skia/BUILD.gn +++ b/starboard/raspi/2/skia/BUILD.gn @@ -22,3 +22,9 @@ static_library("starboard_platform") { configs += [ "//starboard/build/config:starboard_implementation" ] public_deps = [ "//starboard/raspi/shared:starboard_platform" ] } + +if (build_with_separate_cobalt_toolchain) { + group("starboard_platform_with_main") { + deps = [ "//starboard/raspi/2:starboard_platform_with_main" ] + } +} diff --git a/starboard/raspi/2/skia/starboard_loader.cc b/starboard/raspi/2/skia/starboard_loader.cc new file mode 100644 index 000000000000..065bfaa07123 --- /dev/null +++ b/starboard/raspi/2/skia/starboard_loader.cc @@ -0,0 +1,19 @@ +// Copyright 2023 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. + +#include "starboard/event.h" + +int main(int argc, char** argv) { + return SbRunStarboardMain(argc, argv, SbEventHandle); +} diff --git a/starboard/raspi/2/skia/toolchain/BUILD.gn b/starboard/raspi/2/skia/toolchain/BUILD.gn index 3fa9a349b184..8524670452e2 100644 --- a/starboard/raspi/2/skia/toolchain/BUILD.gn +++ b/starboard/raspi/2/skia/toolchain/BUILD.gn @@ -14,6 +14,7 @@ import("//build/config/clang/clang.gni") import("//build/toolchain/gcc_toolchain.gni") +import("//starboard/build/toolchain/cobalt_toolchains.gni") import("//starboard/raspi/shared/toolchain/raspi_shared_toolchain.gni") gcc_toolchain("target") { @@ -29,3 +30,23 @@ gcc_toolchain("target") { is_clang = false } } + +cobalt_clang_toolchain("cobalt") { + variables = { + native_linker_path = gcc_toolchain_cxx + } +} + +gcc_toolchain("starboard") { + cc = gcc_toolchain_cc + cxx = gcc_toolchain_cxx + ld = cxx + + ar = gcc_toolchain_ar + + tail_lib_dependencies = "-l:libpthread.so.0 -l:libdl.so.2" + + toolchain_args = { + is_clang = false + } +} diff --git a/starboard/raspi/shared/launcher.py b/starboard/raspi/shared/launcher.py index 6aa6c46ef0fc..455467b17a3d 100644 --- a/starboard/raspi/shared/launcher.py +++ b/starboard/raspi/shared/launcher.py @@ -29,8 +29,6 @@ from starboard.tools import abstract_launcher from starboard.raspi.shared import retry -IS_MODULAR_BUILD = os.getenv('MODULAR_BUILD', '0') == '1' - class TargetPathError(ValueError): pass @@ -146,9 +144,9 @@ def _GetAndCheckTestFile(self, target_name): def _GetAndCheckTestFileWithFallback(self): try: return self._GetAndCheckTestFile(self.target_name + '_loader') - except TargetPathError as e: - if IS_MODULAR_BUILD: - raise e + except TargetPathError: + # For starboard level test targets like player_filter_tests built as an + # executable, return the target name. return self._GetAndCheckTestFile(self.target_name) def _InitPexpectCommands(self): diff --git a/starboard/raspi/shared/test_filters.py b/starboard/raspi/shared/test_filters.py index d6aeb5549d06..21a428f06ec2 100644 --- a/starboard/raspi/shared/test_filters.py +++ b/starboard/raspi/shared/test_filters.py @@ -13,21 +13,9 @@ # limitations under the License. """Starboard Raspberry Pi Platform Test Filters.""" -import os from starboard.tools.testing import test_filter # pylint: disable=line-too-long -_MODULAR_BUILD_FILTERED_TESTS = { - 'nplb': [ - 'SbSystemGetStackTest.SunnyDayStackDirection', - 'SbSystemGetStackTest.SunnyDay', - 'SbSystemGetStackTest.SunnyDayShortStack', - 'SbSystemSymbolizeTest.SunnyDay' - 'MemoryReportingTest.CapturesOperatorDeleteNothrow', - 'SbAudioSinkTest.*', 'SbDrmTest.AnySupportedKeySystems' - ], - 'player_filter_tests': [test_filter.FILTER_ALL], -} _FILTERED_TESTS = { 'nplb': [ @@ -62,8 +50,6 @@ 'PlayerComponentsTests/PlayerComponentsTest.*', ], } -if os.getenv('MODULAR_BUILD', '0') == '1': - _FILTERED_TESTS = _MODULAR_BUILD_FILTERED_TESTS class TestFilters(object): From 0716d3cd5efb4a705d4be1028833445c88a7bde6 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Fri, 27 Oct 2023 16:33:13 -0700 Subject: [PATCH 123/140] Cherry pick PR #1861: Add getOpenedCobaltService method on StarboardBridge. (#1864) Refer to the original PR: https://github.com/youtube/cobalt/pull/1861 We need to access the opened CobaltService to proactively request Kabuki to process events. b/306034069 Co-authored-by: Colin Liang --- .../app/src/main/java/dev/cobalt/coat/StarboardBridge.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java index d32cf59f29d7..5d6e8f9a4432 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/coat/StarboardBridge.java @@ -783,6 +783,10 @@ CobaltService openCobaltService(long nativeService, String serviceName) { return service; } + public CobaltService getOpenedCobaltService(String serviceName) { + return cobaltServices.get(serviceName); + } + @SuppressWarnings("unused") @UsedByNative void closeCobaltService(String serviceName) { From 7b93f93d00feaca349425ea59b868e09093de2fc Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 30 Oct 2023 10:59:29 -0700 Subject: [PATCH 124/140] Cherry pick PR #1342: [Win] Move references to the XDK to internal (#1856) Refer to the original PR: https://github.com/youtube/cobalt/pull/1342 Make references to the XDK internal-only to allow the external version to build properly. b/296290769 Change-Id: I3bf4c59de1fd3163028d713586294a2a93f403be Co-authored-by: Tyler Holcombe --- starboard/win/shared/BUILD.gn | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/starboard/win/shared/BUILD.gn b/starboard/win/shared/BUILD.gn index 2b0f501f8c01..a99e7e88227e 100644 --- a/starboard/win/shared/BUILD.gn +++ b/starboard/win/shared/BUILD.gn @@ -30,16 +30,18 @@ config("starboard_platform_config") { cflags = [ "/EHsc", # C++ exceptions (required with /ZW) - ] - - cflags += [ "/FU${msvc_path}/lib/x86/store/references/platform.winmd", "/FU${windows_sdk_path}/References/$wdk_version/Windows.Foundation.FoundationContract/4.0.0.0/Windows.Foundation.FoundationContract.winmd", "/FU${windows_sdk_path}/References/$wdk_version/Windows.Foundation.UniversalApiContract/14.0.0.0/Windows.Foundation.UniversalApiContract.winmd", "/FU${windows_sdk_path}/References/$wdk_version/Windows.UI.ViewManagement.ViewManagementViewScalingContract/1.0.0.0/Windows.UI.ViewManagement.ViewManagementViewScalingContract.winmd", - "/FU${windows_sdk_path}/References/$wdk_version/Windows.Xbox.ApplicationResourcesContract/2.0.0.0/Windows.Xbox.ApplicationResourcesContract.winmd", - "/FU${windows_sdk_path}/References/$wdk_version/Windows.Xbox.Security.ApplicationSpecificDeviceAuthenticationContract/1.0.0.0/Windows.Xbox.Security.ApplicationSpecificDeviceAuthenticationContract.winmd", ] + + if (is_internal_build) { + cflags += [ + "/FU${windows_sdk_path}/References/$wdk_version/Windows.Xbox.ApplicationResourcesContract/2.0.0.0/Windows.Xbox.ApplicationResourcesContract.winmd", + "/FU${windows_sdk_path}/References/$wdk_version/Windows.Xbox.Security.ApplicationSpecificDeviceAuthenticationContract/1.0.0.0/Windows.Xbox.Security.ApplicationSpecificDeviceAuthenticationContract.winmd", + ] + } } static_library("starboard_platform") { From 050718757d769b0b5226cff8786355a099814982 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:41:07 -0700 Subject: [PATCH 125/140] Cherry pick PR #1749: Update the SbTimezoneGetName documentation. (#1756) Refer to the original PR: https://github.com/youtube/cobalt/pull/1749 - Clearly document that the implementation should return the time zone name in the IANA format. b/302569322 Change-Id: Ibd6e29f23cb6293f6339fe5a7d860eeb3a9a2b5d Co-authored-by: y4vor --- starboard/time_zone.h | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/starboard/time_zone.h b/starboard/time_zone.h index ce9e4248760b..a8ac53bc1cf4 100644 --- a/starboard/time_zone.h +++ b/starboard/time_zone.h @@ -35,12 +35,11 @@ typedef int SbTimeZone; // Gets the system's current SbTimeZone in minutes. SB_EXPORT SbTimeZone SbTimeZoneGetCurrent(); -// Gets a string representation of the current timezone. Note that the string -// representation can either be standard or daylight saving time. The output -// can be of the form: -// 1) A three-letter abbreviation such as "PST" or "PDT" (preferred). -// 2) A time zone identifier such as "America/Los_Angeles" -// 3) An un-abbreviated name such as "Pacific Standard Time". +// Gets a string representation of the current timezone. The format should be +// in the IANA format https://data.iana.org/time-zones/theory.html#naming . +// Names normally have the form AREA/LOCATION, where AREA is a continent or +// ocean, and LOCATION is a specific location within the area. +// Typical names are 'Africa/Cairo', 'America/New_York', and 'Pacific/Honolulu'. SB_EXPORT const char* SbTimeZoneGetName(); #ifdef __cplusplus From 67568396b368c0b832304c8ccafc6c3c47f1091b Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 1 Nov 2023 14:49:59 -0700 Subject: [PATCH 126/140] Cherry pick PR #1615: Add SetTimeZone API as a starboard extension (#1887) Refer to the original PR: https://github.com/youtube/cobalt/pull/1615 In order to test SbTimeZoneGetCurrent(), we need to set a time zone first. Here we add a new time zone API as a starboard extension to set time zone. We start with Linux implementation. Will have follow up PR to add implementations for all first party platforms. This will help catch the issue if a platform forget to implement SbTimeZoneGetCurrent() which always return 0. b/300108997 Co-authored-by: Sherry Zhou --- starboard/extension/extension_test.cc | 21 +++++++ starboard/extension/time_zone.h | 47 ++++++++++++++ starboard/linux/shared/BUILD.gn | 2 + .../linux/shared/system_get_extensions.cc | 5 ++ starboard/linux/shared/time_zone.cc | 60 ++++++++++++++++++ starboard/linux/shared/time_zone.h | 27 ++++++++ starboard/nplb/time_zone_get_current_test.cc | 63 ++++++++++++++++++- 7 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 starboard/extension/time_zone.h create mode 100644 starboard/linux/shared/time_zone.cc create mode 100644 starboard/linux/shared/time_zone.h diff --git a/starboard/extension/extension_test.cc b/starboard/extension/extension_test.cc index d32cc8436d7f..0a40eef7c361 100644 --- a/starboard/extension/extension_test.cc +++ b/starboard/extension/extension_test.cc @@ -27,6 +27,7 @@ #include "starboard/extension/memory_mapped_file.h" #include "starboard/extension/platform_info.h" #include "starboard/extension/platform_service.h" +#include "starboard/extension/time_zone.h" #include "starboard/extension/updater_notification.h" #include "starboard/extension/url_fetcher_observer.h" #include "starboard/system.h" @@ -438,5 +439,25 @@ TEST(ExtensionTest, PlatformInfo) { << "Extension struct should be a singleton"; } +TEST(ExtensionTest, TimeZone) { + typedef StarboardExtensionTimeZoneApi ExtensionApi; + const char* kExtensionName = kStarboardExtensionTimeZoneName; + + const ExtensionApi* extension_api = + static_cast(SbSystemGetExtension(kExtensionName)); + if (!extension_api) { + return; + } + + EXPECT_STREQ(extension_api->name, kExtensionName); + EXPECT_EQ(extension_api->version, 1u); + EXPECT_NE(extension_api->SetTimeZone, nullptr); + + const ExtensionApi* second_extension_api = + static_cast(SbSystemGetExtension(kExtensionName)); + EXPECT_EQ(second_extension_api, extension_api) + << "Extension struct should be a singleton"; +} + } // namespace extension } // namespace starboard diff --git a/starboard/extension/time_zone.h b/starboard/extension/time_zone.h new file mode 100644 index 000000000000..bfddcea3a4f7 --- /dev/null +++ b/starboard/extension/time_zone.h @@ -0,0 +1,47 @@ +// Copyright 2023 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_EXTENSION_TIME_ZONE_H_ +#define STARBOARD_EXTENSION_TIME_ZONE_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define kStarboardExtensionTimeZoneName "dev.starboard.extension.TimeZone" + +typedef struct StarboardExtensionTimeZoneApi { + // Name should be the string |kStarboardExtensionSetTimeZoneName|. + // This helps to validate that the extension API is correct. + const char* name; + + // This specifies the version of the API that is implemented. + uint32_t version; + + // Sets the current time zone to the specified time zone name. + // Note: This function should not be called with a NULL or empty + // string. It does not actually change the system clock, so it + // will not affect the time displayed on the system clock or + // used by other system processes. + bool (*SetTimeZone)(const char* time_zone_name); + +} StarboardExtensionTimeZoneApi; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // STARBOARD_EXTENSION_TIME_ZONE_H_ diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn index 57b7908261f2..237e75fb1b40 100644 --- a/starboard/linux/shared/BUILD.gn +++ b/starboard/linux/shared/BUILD.gn @@ -80,6 +80,8 @@ static_library("starboard_platform_sources") { "//starboard/linux/shared/system_get_extensions.cc", "//starboard/linux/shared/system_get_path.cc", "//starboard/linux/shared/system_has_capability.cc", + "//starboard/linux/shared/time_zone.cc", + "//starboard/linux/shared/time_zone.h", "//starboard/shared/alsa/alsa_audio_sink_type.cc", "//starboard/shared/alsa/alsa_audio_sink_type.h", "//starboard/shared/alsa/alsa_util.cc", diff --git a/starboard/linux/shared/system_get_extensions.cc b/starboard/linux/shared/system_get_extensions.cc index 4a0b2805e7e4..e1a44b4fe62c 100644 --- a/starboard/linux/shared/system_get_extensions.cc +++ b/starboard/linux/shared/system_get_extensions.cc @@ -22,7 +22,9 @@ #include "starboard/extension/free_space.h" #include "starboard/extension/memory_mapped_file.h" #include "starboard/extension/platform_service.h" +#include "starboard/extension/time_zone.h" #include "starboard/linux/shared/soft_mic_platform_service.h" +#include "starboard/linux/shared/time_zone.h" #include "starboard/shared/enhanced_audio/enhanced_audio.h" #include "starboard/shared/ffmpeg/ffmpeg_demuxer.h" #include "starboard/shared/posix/free_space.h" @@ -74,5 +76,8 @@ const void* SbSystemGetExtension(const char* name) { return use_ffmpeg_demuxer ? starboard::shared::ffmpeg::GetFFmpegDemuxerApi() : NULL; } + if (strcmp(name, kStarboardExtensionTimeZoneName) == 0) { + return starboard::shared::GetTimeZoneApi(); + } return NULL; } diff --git a/starboard/linux/shared/time_zone.cc b/starboard/linux/shared/time_zone.cc new file mode 100644 index 000000000000..30919af1107b --- /dev/null +++ b/starboard/linux/shared/time_zone.cc @@ -0,0 +1,60 @@ +// Copyright 2023 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. + +#include "starboard/linux/shared/time_zone.h" + +#include "starboard/extension/time_zone.h" + +#include +#include +#include + +#include "starboard/common/log.h" + +namespace starboard { +namespace shared { + +namespace { + +// Definitions of any functions included as components in the extension +// are added here. + +bool SetTimeZone(const char* time_zone_name) { + if (time_zone_name == nullptr || strlen(time_zone_name) == 0) { + SB_LOG(ERROR) << "Set time zone failed!"; + SB_LOG(ERROR) << "Time zone name can't be null or empty string."; + return false; + } + if (setenv("TZ", time_zone_name, 1) != 0) { + SB_LOG(WARNING) << "Set time zone failed!"; + return false; + } + tzset(); + return true; +} + +const StarboardExtensionTimeZoneApi kTimeZoneApi = { + kStarboardExtensionTimeZoneName, + 1, // API version that's implemented. + &SetTimeZone, +}; + +} // namespace + +const void* GetTimeZoneApi() { + return &kTimeZoneApi; +} + +} // namespace shared +} // namespace starboard diff --git a/starboard/linux/shared/time_zone.h b/starboard/linux/shared/time_zone.h new file mode 100644 index 000000000000..6f063b0f2d41 --- /dev/null +++ b/starboard/linux/shared/time_zone.h @@ -0,0 +1,27 @@ +// Copyright 2023 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_LINUX_SHARED_TIME_ZONE_H_ +#define STARBOARD_LINUX_SHARED_TIME_ZONE_H_ + +// Omit namespace linux due to symbol name conflict. +namespace starboard { +namespace shared { + +const void* GetTimeZoneApi(); + +} // namespace shared +} // namespace starboard + +#endif // STARBOARD_LINUX_SHARED_TIME_ZONE_H_ diff --git a/starboard/nplb/time_zone_get_current_test.cc b/starboard/nplb/time_zone_get_current_test.cc index 0c136d0f1fe6..0c2b3b86d646 100644 --- a/starboard/nplb/time_zone_get_current_test.cc +++ b/starboard/nplb/time_zone_get_current_test.cc @@ -12,7 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include "starboard/extension/time_zone.h" #include "starboard/nplb/time_constants.h" +#include "starboard/system.h" #include "starboard/time_zone.h" #include "testing/gtest/include/gtest/gtest.h" @@ -34,9 +36,64 @@ TEST(SbTimeZoneGetCurrentTest, IsKindOfSane) { // ... and +24 hours from the Prime Meridian, inclusive EXPECT_LE(zone, 24 * 60); - if (zone == 0) { - SB_LOG(WARNING) << "SbTimeZoneGetCurrent() returns 0. This is only correct " - "if the current time zone is the same as UTC"; + static auto const* time_zone_extension = + static_cast( + SbSystemGetExtension(kStarboardExtensionTimeZoneName)); + if (time_zone_extension) { + ASSERT_STREQ(time_zone_extension->name, kStarboardExtensionTimeZoneName); + ASSERT_EQ(time_zone_extension->version, 1u); + time_zone_extension->SetTimeZone("UTC"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 0); + + // Atlantic time zone, UTC−04:00 + time_zone_extension->SetTimeZone("America/Puerto_Rico"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 240); + + // Eastern time zone, UTC−05:00 + time_zone_extension->SetTimeZone("America/New_York"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 300); + + time_zone_extension->SetTimeZone("US/Eastern"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 300); + + // Central time zone, UTC−06:00 + time_zone_extension->SetTimeZone("America/Chicago"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 360); + + // Mountain time zone, UTC−07:00 + time_zone_extension->SetTimeZone("US/Mountain"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 420); + + // Pacific time zone, UTC-08:00 + time_zone_extension->SetTimeZone("US/Pacific"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 480); + + // Alaska time zone, UTC-09:00 + time_zone_extension->SetTimeZone("US/Alaska"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 540); + + // Hawaii-Aleutian time zone, UTC-10:00 + time_zone_extension->SetTimeZone("Pacific/Honolulu"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 600); + + // American Samoa time zone, UTC-11:00 + time_zone_extension->SetTimeZone("US/Samoa"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, 660); + + // American Samoa time zone, UTC+10:00 + time_zone_extension->SetTimeZone("Pacific/Guam"); + zone = SbTimeZoneGetCurrent(); + EXPECT_EQ(zone, -600); } } From f8fea8fa6a4dcdc725b81c37246c2b7c17ab6b31 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 1 Nov 2023 16:04:31 -0700 Subject: [PATCH 127/140] Cherry pick PR #1633: Add SetTimeZone API for Raspi (#1888) Refer to the original PR: https://github.com/youtube/cobalt/pull/1633 b/300108997 Co-authored-by: Sherry Zhou --- starboard/raspi/shared/BUILD.gn | 2 ++ starboard/raspi/shared/system_get_extensions.cc | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/starboard/raspi/shared/BUILD.gn b/starboard/raspi/shared/BUILD.gn index 2ea2e8413383..8d797b300db7 100644 --- a/starboard/raspi/shared/BUILD.gn +++ b/starboard/raspi/shared/BUILD.gn @@ -35,6 +35,8 @@ static_library("starboard_platform_sources") { "//starboard/linux/shared/system_get_connection_type.cc", "//starboard/linux/shared/system_get_path.cc", "//starboard/linux/shared/system_has_capability.cc", + "//starboard/linux/shared/time_zone.cc", + "//starboard/linux/shared/time_zone.h", "//starboard/raspi/shared/application_dispmanx.cc", "//starboard/raspi/shared/audio_sink_type_dispatcher.cc", "//starboard/raspi/shared/dispmanx_util.cc", diff --git a/starboard/raspi/shared/system_get_extensions.cc b/starboard/raspi/shared/system_get_extensions.cc index bd7854d66915..511fb793d14e 100644 --- a/starboard/raspi/shared/system_get_extensions.cc +++ b/starboard/raspi/shared/system_get_extensions.cc @@ -18,11 +18,13 @@ #include "starboard/extension/configuration.h" #include "starboard/extension/crash_handler.h" #include "starboard/extension/graphics.h" +#include "starboard/extension/time_zone.h" #include "starboard/shared/starboard/crash_handler.h" #if SB_IS(EVERGREEN_COMPATIBLE) #include "starboard/elf_loader/evergreen_config.h" #endif +#include "starboard/linux/shared/time_zone.h" #include "starboard/raspi/shared/configuration.h" #include "starboard/raspi/shared/graphics.h" @@ -48,5 +50,8 @@ const void* SbSystemGetExtension(const char* name) { if (strcmp(name, kCobaltExtensionCrashHandlerName) == 0) { return starboard::common::GetCrashHandlerApi(); } + if (strcmp(name, kStarboardExtensionTimeZoneName) == 0) { + return starboard::shared::GetTimeZoneApi(); + } return NULL; } From 96dc8d3434de54b8533964e5e65b430ebbfab10f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 1 Nov 2023 17:55:15 -0700 Subject: [PATCH 128/140] Cherry pick PR #1664: Correct Linux implementation of SbTimezoneGetName (#1886) Refer to the original PR: https://github.com/youtube/cobalt/pull/1664 b/302569322 Change-Id: Icce69ed2db148020c44ab9d95af8d83283e3c138 Co-authored-by: johnxwork <44791188+johnxwork@users.noreply.github.com> Co-authored-by: y4vor --- starboard/linux/shared/BUILD.gn | 2 +- starboard/nplb/time_zone_get_name_test.cc | 30 ++++++++ starboard/raspi/shared/BUILD.gn | 2 +- starboard/shared/linux/time_zone_get_name.cc | 75 ++++++++++++++++++++ starboard/shared/posix/time_zone_get_name.cc | 24 ------- starboard/time_zone.h | 2 +- starboard/win/win32/test_filters.py | 4 ++ 7 files changed, 112 insertions(+), 27 deletions(-) create mode 100644 starboard/shared/linux/time_zone_get_name.cc delete mode 100644 starboard/shared/posix/time_zone_get_name.cc diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn index 237e75fb1b40..536bfe554509 100644 --- a/starboard/linux/shared/BUILD.gn +++ b/starboard/linux/shared/BUILD.gn @@ -135,6 +135,7 @@ static_library("starboard_platform_sources") { "//starboard/shared/linux/thread_get_id.cc", "//starboard/shared/linux/thread_get_name.cc", "//starboard/shared/linux/thread_set_name.cc", + "//starboard/shared/linux/time_zone_get_name.cc", "//starboard/shared/nouser/user_get_current.cc", "//starboard/shared/nouser/user_get_property.cc", "//starboard/shared/nouser/user_get_signed_in.cc", @@ -219,7 +220,6 @@ static_library("starboard_platform_sources") { "//starboard/shared/posix/time_get_now.cc", "//starboard/shared/posix/time_is_time_thread_now_supported.cc", "//starboard/shared/posix/time_zone_get_current.cc", - "//starboard/shared/posix/time_zone_get_name.cc", "//starboard/shared/pthread/condition_variable_broadcast.cc", "//starboard/shared/pthread/condition_variable_create.cc", "//starboard/shared/pthread/condition_variable_destroy.cc", diff --git a/starboard/nplb/time_zone_get_name_test.cc b/starboard/nplb/time_zone_get_name_test.cc index a3e013caff6d..318cbfc60b61 100644 --- a/starboard/nplb/time_zone_get_name_test.cc +++ b/starboard/nplb/time_zone_get_name_test.cc @@ -12,10 +12,19 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include + +#include "starboard/common/log.h" +#include "starboard/extension/time_zone.h" #include "starboard/nplb/time_constants.h" +#include "starboard/system.h" #include "starboard/time_zone.h" +#include "testing/gmock/include/gmock/gmock.h" #include "testing/gtest/include/gtest/gtest.h" +using testing::AnyOf; +using testing::MatchesRegex; + namespace starboard { namespace nplb { namespace { @@ -41,6 +50,27 @@ TEST(SbTimeZoneGetNameTest, IsKindOfSane) { // ":Pacific/Kiritimati" is the western-most timezone at UTC+14. } +TEST(SbTimeZoneGetNameTest, IsIANAFormat) { + const char* name = SbTimeZoneGetName(); + SB_LOG(INFO) << "time zone name: " << name; + char cpy[100]; + snprintf(cpy, sizeof(cpy), "%s", name); + char* continent = strtok(cpy, "/"); + // The time zone ID starts with a Continent or Ocean name. + EXPECT_THAT( + continent, + testing::AnyOf(std::string("Asia"), std::string("America"), + std::string("Africa"), std::string("Europe"), + std::string("Australia"), std::string("Pacific"), + std::string("Atlantic"), std::string("Antarctica"), + // time zone can be "Etc/UTC" if unset(such as on + // CI builders), shouldn't happen in production. + // TODO(b/304351956): Remove Etc after fixing builders. + std::string("Indian"), std::string("Etc"))); + char* city = strtok(NULL, "/"); + EXPECT_TRUE(strlen(city) != 0); +} + } // namespace } // namespace nplb } // namespace starboard diff --git a/starboard/raspi/shared/BUILD.gn b/starboard/raspi/shared/BUILD.gn index 8d797b300db7..39ca20e177c6 100644 --- a/starboard/raspi/shared/BUILD.gn +++ b/starboard/raspi/shared/BUILD.gn @@ -113,6 +113,7 @@ static_library("starboard_platform_sources") { "//starboard/shared/linux/thread_get_id.cc", "//starboard/shared/linux/thread_get_name.cc", "//starboard/shared/linux/thread_set_name.cc", + "//starboard/shared/linux/time_zone_get_name.cc", "//starboard/shared/nouser/user_get_current.cc", "//starboard/shared/nouser/user_get_property.cc", "//starboard/shared/nouser/user_get_signed_in.cc", @@ -189,7 +190,6 @@ static_library("starboard_platform_sources") { "//starboard/shared/posix/time_get_now.cc", "//starboard/shared/posix/time_is_time_thread_now_supported.cc", "//starboard/shared/posix/time_zone_get_current.cc", - "//starboard/shared/posix/time_zone_get_name.cc", "//starboard/shared/pthread/condition_variable_broadcast.cc", "//starboard/shared/pthread/condition_variable_create.cc", "//starboard/shared/pthread/condition_variable_destroy.cc", diff --git a/starboard/shared/linux/time_zone_get_name.cc b/starboard/shared/linux/time_zone_get_name.cc new file mode 100644 index 000000000000..4b1819edb82f --- /dev/null +++ b/starboard/shared/linux/time_zone_get_name.cc @@ -0,0 +1,75 @@ +// Copyright 2015 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. + +#include +#include +#include + +#include "starboard/common/log.h" +#include "starboard/time_zone.h" + +#define TZDEFAULT "/etc/localtime" +#define TZZONEINFOTAIL "/zoneinfo/" +#define isNonDigit(ch) (ch < '0' || '9' < ch) + +static char gTimeZoneBuffer[PATH_MAX]; +static char* gTimeZoneBufferPtr = NULL; + +static bool isValidOlsonID(const char* id) { + int32_t idx = 0; + + /* Determine if this is something like Iceland (Olson ID) + or AST4ADT (non-Olson ID) */ + while (id[idx] && isNonDigit(id[idx]) && id[idx] != ',') { + idx++; + } + + /* If we went through the whole string, then it might be okay. + The timezone is sometimes set to "CST-7CDT", "CST6CDT5,J129,J131/19:30", + "GRNLNDST3GRNLNDDT" or similar, so we cannot use it. + The rest of the time it could be an Olson ID. George */ + return static_cast(id[idx] == 0 || strcmp(id, "PST8PDT") == 0 || + strcmp(id, "MST7MDT") == 0 || + strcmp(id, "CST6CDT") == 0 || + strcmp(id, "EST5EDT") == 0); +} + +// Similar to how ICU::putil.cpp gets IANA(Olsen) timezone ID. +const char* SbTimeZoneGetName() { + /* + This is a trick to look at the name of the link to get the Olson ID + because the tzfile contents is underspecified. + This isn't guaranteed to work because it may not be a symlink. + But this is production-tested solution for most versions of Linux. + */ + + if (gTimeZoneBufferPtr == NULL) { + int32_t ret = (int32_t)readlink(TZDEFAULT, gTimeZoneBuffer, + sizeof(gTimeZoneBuffer) - 1); + if (0 < ret) { + int32_t tzZoneInfoTailLen = strlen(TZZONEINFOTAIL); + gTimeZoneBuffer[ret] = 0; + char* tzZoneInfoTailPtr = strstr(gTimeZoneBuffer, TZZONEINFOTAIL); + + if (tzZoneInfoTailPtr != NULL && + isValidOlsonID(tzZoneInfoTailPtr + tzZoneInfoTailLen)) { + return (gTimeZoneBufferPtr = tzZoneInfoTailPtr + tzZoneInfoTailLen); + } + } + SB_NOTREACHED(); + return ""; + } else { + return gTimeZoneBufferPtr; + } +} diff --git a/starboard/shared/posix/time_zone_get_name.cc b/starboard/shared/posix/time_zone_get_name.cc deleted file mode 100644 index 5a1e013c4481..000000000000 --- a/starboard/shared/posix/time_zone_get_name.cc +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2015 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. - -#include "starboard/time_zone.h" - -#include - -const char* SbTimeZoneGetName() { - // TODO: Using tzname assumes that tzset() has been called at some - // point. That should happen as part of Starboard's main loop initialization, - // but that doesn't exist yet. - return tzname[0]; -} diff --git a/starboard/time_zone.h b/starboard/time_zone.h index a8ac53bc1cf4..d140338b58fd 100644 --- a/starboard/time_zone.h +++ b/starboard/time_zone.h @@ -29,7 +29,7 @@ extern "C" { // The number of minutes west of the Greenwich Prime Meridian, NOT including // Daylight Savings Time adjustments. // -// For example: PST/PDT is 480 minutes (28800 seconds, 8 hours). +// For example: America/Los_Angeles is 480 minutes (28800 seconds, 8 hours). typedef int SbTimeZone; // Gets the system's current SbTimeZone in minutes. diff --git a/starboard/win/win32/test_filters.py b/starboard/win/win32/test_filters.py index cf8621fb81b0..9ab3ffd84f04 100644 --- a/starboard/win/win32/test_filters.py +++ b/starboard/win/win32/test_filters.py @@ -53,6 +53,10 @@ # Enable once verified on the platform. 'SbMediaCanPlayMimeAndKeySystem.MinimumSupport', + + # Windows uses a special time zone format that ICU accepts, so we don't enforce IANA. + # TODO(b/304335954): Re-enable the test after fixing Windows implementation. + 'SbTimeZoneGetNameTest.IsIANAFormat', ], 'player_filter_tests': [ # These tests fail on our VMs for win-win32 builds due to missing From 6586e24ec4fe9acebbadabe33a8369e21c1bb1c9 Mon Sep 17 00:00:00 2001 From: Kaido Kert Date: Thu, 2 Nov 2023 18:20:36 -0700 Subject: [PATCH 129/140] Update LTS minor version to 20 (#1897) b/260110906 --- cobalt/version.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cobalt/version.h b/cobalt/version.h index 2620123cedde..f54bb3be468a 100644 --- a/cobalt/version.h +++ b/cobalt/version.h @@ -35,6 +35,6 @@ // release is cut. //. -#define COBALT_VERSION "24.lts.14" +#define COBALT_VERSION "24.lts.20" #endif // COBALT_VERSION_H_ From 25ca7bf59ba4eacf37899c62f27991905a4dbd72 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Sun, 5 Nov 2023 20:19:32 -0800 Subject: [PATCH 130/140] Cherry pick PR #1765: [XB1] Fix skipped OnDecoderDrained task. (#1912) Refer to the original PR: https://github.com/youtube/cobalt/pull/1765 b/284359403 Co-authored-by: victorpasoshnikov --- starboard/xb1/shared/gpu_base_video_decoder.cc | 7 +++---- starboard/xb1/shared/gpu_base_video_decoder.h | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/starboard/xb1/shared/gpu_base_video_decoder.cc b/starboard/xb1/shared/gpu_base_video_decoder.cc index a37dc716f459..155fef26eca3 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.cc +++ b/starboard/xb1/shared/gpu_base_video_decoder.cc @@ -593,9 +593,6 @@ void GpuVideoDecoderBase::OnDecoderDrained() { decoder_behavior_.load() == kResettingDecoder); is_waiting_frame_after_drain_ = true; - if (decoder_behavior_.load() == kResettingDecoder || error_occured_) { - return; - } if (!BelongsToDecoderThread()) { decoder_thread_->job_queue()->Schedule( @@ -603,7 +600,6 @@ void GpuVideoDecoderBase::OnDecoderDrained() { return; } - SB_DCHECK(written_inputs_.empty()); if (decoder_behavior_.load() == kEndingStream) { decoder_status_cb_(kBufferFull, VideoFrame::CreateEOSFrame()); } @@ -688,6 +684,9 @@ void GpuVideoDecoderBase::DrainDecoder() { if (!is_drain_decoder_called_) { is_drain_decoder_called_ = true; DrainDecoderInternal(); + // DrainDecoderInternal is sync command, after it finished, we can be sure + // that drain really completed. + OnDecoderDrained(); } } diff --git a/starboard/xb1/shared/gpu_base_video_decoder.h b/starboard/xb1/shared/gpu_base_video_decoder.h index 1853a640fc1d..34d45338813d 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.h +++ b/starboard/xb1/shared/gpu_base_video_decoder.h @@ -218,7 +218,7 @@ class GpuVideoDecoderBase Mutex frame_buffers_mutex_; ConditionVariable frame_buffers_condition_; - // static std::vector> s_frame_buffers_; + private: class GPUDecodeTargetPrivate; From 932032cdc68976c38476b92aceb6567102f1dead Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 6 Nov 2023 06:27:01 -0800 Subject: [PATCH 131/140] Cherry pick PR #1859: Fix typo in log statements: Crashapd -> Crashpad (#1915) Refer to the original PR: https://github.com/youtube/cobalt/pull/1859 b/306218562 Change-Id: Id5fa3d725d7a180ef7221a5b460c1b29cf505a02 Co-authored-by: Holden Warriner --- starboard/elf_loader/sandbox.cc | 2 +- starboard/loader_app/loader_app.cc | 2 +- starboard/loader_app/slot_management.cc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/starboard/elf_loader/sandbox.cc b/starboard/elf_loader/sandbox.cc index 2f2eb059c9e4..d86595e501d1 100644 --- a/starboard/elf_loader/sandbox.cc +++ b/starboard/elf_loader/sandbox.cc @@ -58,7 +58,7 @@ void LoadLibraryAndInitialize(const std::string& library_path, GetEvergreenInfo(&evergreen_info); if (!third_party::crashpad::wrapper::AddEvergreenInfoToCrashpad( evergreen_info)) { - SB_LOG(ERROR) << "Could not send Cobalt library information into Crashapd."; + SB_LOG(ERROR) << "Could not send Cobalt library information into Crashpad."; } else { SB_LOG(INFO) << "Loaded Cobalt library information into Crashpad."; } diff --git a/starboard/loader_app/loader_app.cc b/starboard/loader_app/loader_app.cc index 95f7ece2b70b..93cf9f4e6ec3 100644 --- a/starboard/loader_app/loader_app.cc +++ b/starboard/loader_app/loader_app.cc @@ -143,7 +143,7 @@ void LoadLibraryAndInitialize(const std::string& alternative_content_path, GetEvergreenInfo(&evergreen_info); if (!third_party::crashpad::wrapper::AddEvergreenInfoToCrashpad( evergreen_info)) { - SB_LOG(ERROR) << "Could not send Cobalt library information into Crashapd."; + SB_LOG(ERROR) << "Could not send Cobalt library information into Crashpad."; } else { SB_LOG(INFO) << "Loaded Cobalt library information into Crashpad."; } diff --git a/starboard/loader_app/slot_management.cc b/starboard/loader_app/slot_management.cc index fc5549adf0ac..8b1af986bfdb 100644 --- a/starboard/loader_app/slot_management.cc +++ b/starboard/loader_app/slot_management.cc @@ -289,7 +289,7 @@ void* LoadSlotManagedLibrary(const std::string& app_key, if (!third_party::crashpad::wrapper::AddEvergreenInfoToCrashpad( evergreen_info)) { SB_LOG(ERROR) - << "Could not send Cobalt library information into Crashapd."; + << "Could not send Cobalt library information into Crashpad."; } else { SB_LOG(INFO) << "Loaded Cobalt library information into Crashpad."; } From 2539292bb7c80de9bab5b1737549b6ab05d5267f Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Mon, 6 Nov 2023 10:56:13 -0800 Subject: [PATCH 132/140] Cherry pick PR #1905: [XB1] Revert UWP implementation of SbTimeZoneGetName (#1911) Refer to the original PR: https://github.com/youtube/cobalt/pull/1905 Revert to using the Win32 implementation of SbTimeZoneGetName due to a daylight time offest error. Filter the IsIANAFormat test for Win32 and XB1 b/304335954 Change-Id: I0aec1011bf8475ea7b6a24f755bb5911b55a6255 Co-authored-by: Tyler Holcombe --- starboard/shared/win32/test_filters.py | 10 +++++++++- starboard/win/win32/test_filters.py | 4 ---- starboard/xb1/BUILD.gn | 5 ++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/starboard/shared/win32/test_filters.py b/starboard/shared/win32/test_filters.py index 261946615bba..352adbb1c886 100644 --- a/starboard/shared/win32/test_filters.py +++ b/starboard/shared/win32/test_filters.py @@ -15,7 +15,15 @@ from starboard.tools.testing import test_filter -_FILTERED_TESTS = {} +_FILTERED_TESTS = { + 'nplb': [ + # Windows uses a special time zone format that ICU accepts, so we don't + # enforce IANA. + # TODO(b/304335954): Re-enable the test for UWP after fixing DST + # implementation. + 'SbTimeZoneGetNameTest.IsIANAFormat', + ], +} class TestFilters(object): diff --git a/starboard/win/win32/test_filters.py b/starboard/win/win32/test_filters.py index 9ab3ffd84f04..cf8621fb81b0 100644 --- a/starboard/win/win32/test_filters.py +++ b/starboard/win/win32/test_filters.py @@ -53,10 +53,6 @@ # Enable once verified on the platform. 'SbMediaCanPlayMimeAndKeySystem.MinimumSupport', - - # Windows uses a special time zone format that ICU accepts, so we don't enforce IANA. - # TODO(b/304335954): Re-enable the test after fixing Windows implementation. - 'SbTimeZoneGetNameTest.IsIANAFormat', ], 'player_filter_tests': [ # These tests fail on our VMs for win-win32 builds due to missing diff --git a/starboard/xb1/BUILD.gn b/starboard/xb1/BUILD.gn index a0ad82bc0e2c..200f9cfa454b 100644 --- a/starboard/xb1/BUILD.gn +++ b/starboard/xb1/BUILD.gn @@ -134,7 +134,6 @@ static_library("starboard_platform") { "//starboard/shared/uwp/system_platform_error_internal.cc", "//starboard/shared/uwp/system_platform_error_internal.h", "//starboard/shared/uwp/system_raise_platform_error.cc", - "//starboard/shared/uwp/time_zone_get_name.cc", "//starboard/shared/uwp/wasapi_audio.cc", "//starboard/shared/uwp/wasapi_audio.h", "//starboard/shared/uwp/wasapi_audio_sink.cc", @@ -177,6 +176,10 @@ static_library("starboard_platform") { "//starboard/shared/win32/media_get_max_buffer_capacity.cc", "//starboard/shared/win32/media_transform.cc", "//starboard/shared/win32/media_transform.h", + + # TODO (b/304335954): Use uwp implementation for correct IANA name once + # daylight savings offset is fixed. + "//starboard/shared/win32/time_zone_get_name.cc", "//starboard/shared/win32/video_decoder.cc", "//starboard/shared/win32/video_decoder.h", "//starboard/shared/win32/win32_audio_decoder.cc", From 4a12312205135a8e60283cd04cd04013b2f4cdc8 Mon Sep 17 00:00:00 2001 From: Tyler Holcombe Date: Mon, 6 Nov 2023 15:17:58 -0800 Subject: [PATCH 133/140] Cherry pick PR #1540: [XB1] Replace the av1 decoder with dav1d based GPU (#1922) decoder Refer to the original PR: #1540 b/281831576 --- starboard/shared/uwp/extended_resources_manager.cc | 9 ++++----- starboard/shared/uwp/player_components_factory.cc | 4 ++-- starboard/xb1/BUILD.gn | 6 +++--- starboard/xb1/shared/gpu_base_video_decoder.cc | 1 - 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/starboard/shared/uwp/extended_resources_manager.cc b/starboard/shared/uwp/extended_resources_manager.cc index 4d53de84bca5..21d2821441c7 100644 --- a/starboard/shared/uwp/extended_resources_manager.cc +++ b/starboard/shared/uwp/extended_resources_manager.cc @@ -26,7 +26,7 @@ #include "starboard/time.h" #include "starboard/xb1/shared/internal_shims.h" #if defined(INTERNAL_BUILD) -#include "internal/starboard/xb1/av1_video_decoder.h" +#include "internal/starboard/xb1/dav1d_video_decoder.h" #include "internal/starboard/xb1/vpx_video_decoder.h" #include "third_party/internal/libvpx_xb1/libvpx/d3dx12.h" #endif // defined(INTERNAL_BUILD) @@ -41,7 +41,7 @@ using Microsoft::WRL::ComPtr; using ::starboard::shared::starboard::media::MimeSupportabilityCache; using Windows::Foundation::Metadata::ApiInformation; #if defined(INTERNAL_BUILD) -using ::starboard::xb1::shared::Av1VideoDecoder; +using ::starboard::xb1::shared::Dav1dVideoDecoder; using ::starboard::xb1::shared::GpuVideoDecoderBase; using ::starboard::xb1::shared::VpxVideoDecoder; #endif // defined(INTERNAL_BUILD) @@ -392,8 +392,7 @@ void ExtendedResourcesManager::CompileShadersAsynchronously() { "shader compile."; return; } - if (Av1VideoDecoder::CompileShaders(d3d12device_, d3d12FrameBuffersHeap_, - d3d12queue_.Get())) { + if (Dav1dVideoDecoder::CompileShaders(d3d12device_)) { is_av1_shader_compiled_ = true; SB_LOG(INFO) << "Gpu based AV1 decoder finished compiling its shaders."; } else { @@ -465,7 +464,7 @@ void ExtendedResourcesManager::ReleaseExtendedResourcesInternal() { SB_LOG(INFO) << "CreateEvent() failed with " << GetLastError(); } #if defined(INTERNAL_BUILD) - Av1VideoDecoder::ReleaseShaders(); + Dav1dVideoDecoder::ReleaseShaders(); VpxVideoDecoder::ReleaseShaders(); #endif // #if defined(INTERNAL_BUILD) is_av1_shader_compiled_ = false; diff --git a/starboard/shared/uwp/player_components_factory.cc b/starboard/shared/uwp/player_components_factory.cc index ce7f0d26d463..c8511be2d08b 100644 --- a/starboard/shared/uwp/player_components_factory.cc +++ b/starboard/shared/uwp/player_components_factory.cc @@ -43,7 +43,7 @@ #include "starboard/xb1/shared/video_decoder_uwp.h" #if defined(INTERNAL_BUILD) -#include "internal/starboard/xb1/av1_video_decoder.h" +#include "internal/starboard/xb1/dav1d_video_decoder.h" #include "internal/starboard/xb1/vpx_video_decoder.h" #endif // defined(INTERNAL_BUILD) @@ -254,7 +254,7 @@ class PlayerComponentsFactory : public PlayerComponents::Factory { #if defined(INTERNAL_BUILD) using GpuVp9VideoDecoder = ::starboard::xb1::shared::VpxVideoDecoder; - using GpuAv1VideoDecoder = ::starboard::xb1::shared::Av1VideoDecoder; + using GpuAv1VideoDecoder = ::starboard::xb1::shared::Dav1dVideoDecoder; if (video_codec == kSbMediaVideoCodecVp9) { video_decoder->reset(new GpuVp9VideoDecoder( diff --git a/starboard/xb1/BUILD.gn b/starboard/xb1/BUILD.gn index 200f9cfa454b..e4137bc401b4 100644 --- a/starboard/xb1/BUILD.gn +++ b/starboard/xb1/BUILD.gn @@ -217,8 +217,8 @@ static_library("starboard_platform") { if (is_internal_build) { sources += [ "//internal/starboard/shared/uwp/keys.cc", - "//internal/starboard/xb1/av1_video_decoder.cc", - "//internal/starboard/xb1/av1_video_decoder.h", + "//internal/starboard/xb1/dav1d_video_decoder.cc", + "//internal/starboard/xb1/dav1d_video_decoder.h", "//internal/starboard/xb1/drm_create_system.cc", "//internal/starboard/xb1/internal_shims.cc", "//internal/starboard/xb1/media_is_supported.cc", @@ -238,7 +238,7 @@ static_library("starboard_platform") { "//starboard/shared/widevine:oemcrypto", "//third_party/internal/ce_cdm/cdm:widevine_cdm_core", "//third_party/internal/ce_cdm/cdm:widevine_ce_cdm_static", - "//third_party/internal/libav1_xb1", + "//third_party/internal/dav1d_gpu/xb1:dav1d_xb1", "//third_party/internal/libvpx_xb1", ] } else { diff --git a/starboard/xb1/shared/gpu_base_video_decoder.cc b/starboard/xb1/shared/gpu_base_video_decoder.cc index 155fef26eca3..d2b83b328996 100644 --- a/starboard/xb1/shared/gpu_base_video_decoder.cc +++ b/starboard/xb1/shared/gpu_base_video_decoder.cc @@ -476,7 +476,6 @@ void GpuVideoDecoderBase::Reset() { decoder_thread_->job_queue()->Schedule( std::bind(&GpuVideoDecoderBase::DrainDecoder, this)); decoder_thread_.reset(); - SB_DCHECK(decoder_behavior_.load() == kDecodingStopped); } pending_inputs_.clear(); { From 194524c172d43acbb725601ef17ba952fd458b9c Mon Sep 17 00:00:00 2001 From: Tyler Holcombe Date: Mon, 6 Nov 2023 18:30:14 -0800 Subject: [PATCH 134/140] Cherry pick PR #1894: [XB1] Add d3d_device_ and device_manager_ checks (#1923) Refer to the original PR: #1894 b/284361284 --- starboard/shared/win32/dx_context_video_decoder.cc | 2 +- starboard/shared/win32/video_decoder.cc | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/starboard/shared/win32/dx_context_video_decoder.cc b/starboard/shared/win32/dx_context_video_decoder.cc index 1ca74038cb85..b576659c87bb 100644 --- a/starboard/shared/win32/dx_context_video_decoder.cc +++ b/starboard/shared/win32/dx_context_video_decoder.cc @@ -45,7 +45,7 @@ HardwareDecoderContext GetDirectXForHardwareDecoding() { query_display(display, EGL_DEVICE_EXT, &egl_device); SB_DCHECK(egl_device != 0); - intptr_t device; + intptr_t device = 0; query_device(reinterpret_cast(egl_device), EGL_D3D11_DEVICE_ANGLE, &device); diff --git a/starboard/shared/win32/video_decoder.cc b/starboard/shared/win32/video_decoder.cc index b69cdaf5bb1c..f8890195d88b 100644 --- a/starboard/shared/win32/video_decoder.cc +++ b/starboard/shared/win32/video_decoder.cc @@ -194,6 +194,10 @@ VideoDecoder::VideoDecoder( HardwareDecoderContext hardware_context = GetDirectXForHardwareDecoding(); d3d_device_ = hardware_context.dx_device_out; device_manager_ = hardware_context.dxgi_device_manager_out; + if (!d3d_device_ || !device_manager_) { + return; + } + HRESULT hr = d3d_device_.As(&video_device_); if (FAILED(hr)) { return; From 64def2e8f9b4db65cfdd4f2801a565af99fe8455 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 7 Nov 2023 10:08:20 -0800 Subject: [PATCH 135/140] Cherry pick PR #1929: Add Starboard IFA extension. (#1932) Refer to the original PR: https://github.com/youtube/cobalt/pull/1929 This extension would allow IFA backporting to Starboard version 12 or 13. b/309547847 Change-Id: If3dc2a1eb13377ce33463d2d9bb0b14e33089724 Co-authored-by: y4vor --- cobalt/h5vcc/h5vcc_system.cc | 37 ++++++++++- starboard/extension/extension_test.cc | 22 +++++++ starboard/extension/ifa.h | 58 +++++++++++++++++ starboard/linux/shared/BUILD.gn | 2 + starboard/linux/shared/ifa.cc | 65 +++++++++++++++++++ starboard/linux/shared/ifa.h | 27 ++++++++ .../linux/shared/system_get_extensions.cc | 7 ++ 7 files changed, 216 insertions(+), 2 deletions(-) create mode 100644 starboard/extension/ifa.h create mode 100644 starboard/linux/shared/ifa.cc create mode 100644 starboard/linux/shared/ifa.h diff --git a/cobalt/h5vcc/h5vcc_system.cc b/cobalt/h5vcc/h5vcc_system.cc index 89c9d9a9eac3..addb71767965 100644 --- a/cobalt/h5vcc/h5vcc_system.cc +++ b/cobalt/h5vcc/h5vcc_system.cc @@ -21,6 +21,10 @@ #include "starboard/common/system_property.h" #include "starboard/system.h" +#if SB_API_VERSION < 14 +#include "starboard/extension/ifa.h" +#endif // SB_API_VERSION < 14 + using starboard::kSystemPropertyMaxLength; namespace cobalt { @@ -57,27 +61,56 @@ std::string H5vccSystem::platform() const { std::string H5vccSystem::advertising_id() const { std::string result; -#if SB_API_VERSION >= 14 char property[kSystemPropertyMaxLength] = {0}; +#if SB_API_VERSION >= 14 if (!SbSystemGetProperty(kSbSystemPropertyAdvertisingId, property, SB_ARRAY_SIZE_INT(property))) { DLOG(FATAL) << "Failed to get kSbSystemPropertyAdvertisingId."; } else { result = property; } +#else + static auto const* ifa_extension = + static_cast( + SbSystemGetExtension(kStarboardExtensionIfaName)); + if (ifa_extension && + strcmp(ifa_extension->name, kStarboardExtensionIfaName) == 0 && + ifa_extension->version >= 1) { + if (!ifa_extension->GetAdvertisingId(property, + SB_ARRAY_SIZE_INT(property))) { + DLOG(FATAL) << "Failed to get AdvertisingId from IFA extension."; + } else { + result = property; + } + } #endif return result; } bool H5vccSystem::limit_ad_tracking() const { bool result = false; -#if SB_API_VERSION >= 14 char property[kSystemPropertyMaxLength] = {0}; +#if SB_API_VERSION >= 14 if (!SbSystemGetProperty(kSbSystemPropertyLimitAdTracking, property, SB_ARRAY_SIZE_INT(property))) { DLOG(FATAL) << "Failed to get kSbSystemPropertyAdvertisingId."; } else { result = std::atoi(property); } +#else + static auto const* ifa_extension = + static_cast( + SbSystemGetExtension(kStarboardExtensionIfaName)); + + if (ifa_extension && + strcmp(ifa_extension->name, kStarboardExtensionIfaName) == 0 && + ifa_extension->version >= 1) { + if (!ifa_extension->GetLimitAdTracking(property, + SB_ARRAY_SIZE_INT(property))) { + DLOG(FATAL) << "Failed to get LimitAdTracking from IFA extension."; + } else { + result = std::atoi(property); + } + } #endif return result; } diff --git a/starboard/extension/extension_test.cc b/starboard/extension/extension_test.cc index 0a40eef7c361..874cce11bbfd 100644 --- a/starboard/extension/extension_test.cc +++ b/starboard/extension/extension_test.cc @@ -21,6 +21,7 @@ #include "starboard/extension/font.h" #include "starboard/extension/free_space.h" #include "starboard/extension/graphics.h" +#include "starboard/extension/ifa.h" #include "starboard/extension/installation_manager.h" #include "starboard/extension/javascript_cache.h" #include "starboard/extension/media_session.h" @@ -459,5 +460,26 @@ TEST(ExtensionTest, TimeZone) { << "Extension struct should be a singleton"; } +TEST(ExtensionTest, Ifa) { + typedef StarboardExtensionIfaApi ExtensionApi; + const char* kExtensionName = kStarboardExtensionIfaName; + + const ExtensionApi* extension_api = + static_cast(SbSystemGetExtension(kExtensionName)); + if (!extension_api) { + return; + } + + EXPECT_STREQ(extension_api->name, kExtensionName); + EXPECT_EQ(extension_api->version, 1u); + EXPECT_NE(extension_api->GetAdvertisingId, nullptr); + EXPECT_NE(extension_api->GetLimitAdTracking, nullptr); + + const ExtensionApi* second_extension_api = + static_cast(SbSystemGetExtension(kExtensionName)); + EXPECT_EQ(second_extension_api, extension_api) + << "Extension struct should be a singleton"; +} + } // namespace extension } // namespace starboard diff --git a/starboard/extension/ifa.h b/starboard/extension/ifa.h new file mode 100644 index 000000000000..a2070492dabd --- /dev/null +++ b/starboard/extension/ifa.h @@ -0,0 +1,58 @@ +// Copyright 2023 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_EXTENSION_IFA_H_ +#define STARBOARD_EXTENSION_IFA_H_ + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +#define kStarboardExtensionIfaName "dev.cobalt.extension.Ifa" + +typedef struct StarboardExtensionIfaApi { + // Name should be the string |kCobaltExtensionIfaName|. + // This helps to validate that the extension API is correct. + const char* name; + + // This specifies the version of the API that is implemented. + uint32_t version; + + // The fields below this point were added in version 1 or later. + + // Advertising ID or IFA, typically a 128-bit UUID + // Please see https://iabtechlab.com/OTT-IFA for details. + // Corresponds to 'ifa' field. Note: `ifa_type` field is not provided. + // In Starboard 14 this the value is retrieved through the system + // property `kSbSystemPropertyAdvertisingId` defined in + // `starboard/system.h`. + bool (*GetAdvertisingId)(char* out_value, int value_length); + + // Limit advertising tracking, treated as boolean. Set to nonzero to indicate + // a true value. Corresponds to 'lmt' field. + // In Starboard 14 this the value is retrieved through the system + // property `kSbSystemPropertyLimitAdTracking` defined in + // `starboard/system.h`. + + bool (*GetLimitAdTracking)(char* out_value, int value_length); + +} CobaltExtensionIfaApi; + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // STARBOARD_EXTENSION_IFA_H_ diff --git a/starboard/linux/shared/BUILD.gn b/starboard/linux/shared/BUILD.gn index 536bfe554509..4b4e3fa49101 100644 --- a/starboard/linux/shared/BUILD.gn +++ b/starboard/linux/shared/BUILD.gn @@ -66,6 +66,8 @@ static_library("starboard_platform_sources") { "//starboard/linux/shared/decode_target_internal.cc", "//starboard/linux/shared/decode_target_internal.h", "//starboard/linux/shared/decode_target_release.cc", + "//starboard/linux/shared/ifa.cc", + "//starboard/linux/shared/ifa.h", "//starboard/linux/shared/media_is_audio_supported.cc", "//starboard/linux/shared/media_is_video_supported.cc", "//starboard/linux/shared/netlink.cc", diff --git a/starboard/linux/shared/ifa.cc b/starboard/linux/shared/ifa.cc new file mode 100644 index 000000000000..21b17dc745cd --- /dev/null +++ b/starboard/linux/shared/ifa.cc @@ -0,0 +1,65 @@ +// Copyright 2023 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. + +#include "starboard/linux/shared/ifa.h" + +#include "starboard/extension/ifa.h" + +#include "starboard/common/string.h" +#include "starboard/shared/environment.h" + +namespace starboard { +namespace shared { + +namespace { + +bool CopyStringAndTestIfSuccess(char* out_value, + int value_length, + const char* from_value) { + if (strlen(from_value) + 1 > value_length) + return false; + starboard::strlcpy(out_value, from_value, value_length); + return true; +} + +// Definitions of any functions included as components in the extension +// are added here. + +bool GetAdvertisingId(char* out_value, int value_length) { + return CopyStringAndTestIfSuccess( + out_value, value_length, + starboard::GetEnvironment("COBALT_ADVERTISING_ID").c_str()); +} + +bool GetLimitAdTracking(char* out_value, int value_length) { + return CopyStringAndTestIfSuccess( + out_value, value_length, + GetEnvironment("COBALT_LIMIT_AD_TRACKING").c_str()); +} + +const StarboardExtensionIfaApi kIfaApi = { + kStarboardExtensionIfaName, + 1, // API version that's implemented. + &GetAdvertisingId, + &GetLimitAdTracking, +}; + +} // namespace + +const void* GetIfaApi() { + return &kIfaApi; +} + +} // namespace shared +} // namespace starboard diff --git a/starboard/linux/shared/ifa.h b/starboard/linux/shared/ifa.h new file mode 100644 index 000000000000..fbe61abfeb78 --- /dev/null +++ b/starboard/linux/shared/ifa.h @@ -0,0 +1,27 @@ +// Copyright 2023 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_LINUX_SHARED_IFA_H_ +#define STARBOARD_LINUX_SHARED_IFA_H_ + +// Omit namespace linux due to symbol name conflict. +namespace starboard { +namespace shared { + +const void* GetIfaApi(); + +} // namespace shared +} // namespace starboard + +#endif // STARBOARD_LINUX_SHARED_IFA_H_ diff --git a/starboard/linux/shared/system_get_extensions.cc b/starboard/linux/shared/system_get_extensions.cc index e1a44b4fe62c..efc9169f829d 100644 --- a/starboard/linux/shared/system_get_extensions.cc +++ b/starboard/linux/shared/system_get_extensions.cc @@ -20,9 +20,11 @@ #include "starboard/extension/demuxer.h" #include "starboard/extension/enhanced_audio.h" #include "starboard/extension/free_space.h" +#include "starboard/extension/ifa.h" #include "starboard/extension/memory_mapped_file.h" #include "starboard/extension/platform_service.h" #include "starboard/extension/time_zone.h" +#include "starboard/linux/shared/ifa.h" #include "starboard/linux/shared/soft_mic_platform_service.h" #include "starboard/linux/shared/time_zone.h" #include "starboard/shared/enhanced_audio/enhanced_audio.h" @@ -79,5 +81,10 @@ const void* SbSystemGetExtension(const char* name) { if (strcmp(name, kStarboardExtensionTimeZoneName) == 0) { return starboard::shared::GetTimeZoneApi(); } +#if SB_API_VERSION < 14 + if (strcmp(name, kStarboardExtensionIfaName) == 0) { + return starboard::shared::GetIfaApi(); + } +#endif // SB_API_VERSION < 14 return NULL; } From de97945376f80892949e8c84402293ce4c627186 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:36:40 -0800 Subject: [PATCH 136/140] Cherry pick PR #1900: Avoid running evergreen specific test targets on non-evergreen platforms (#1903) Refer to the original PR: https://github.com/youtube/cobalt/pull/1900 b/302008891 Co-authored-by: Niranjan Yardi --- starboard/tools/testing/test_filter.py | 7 +++++++ starboard/win/win32/test_filters.py | 1 + starboard/xb1/test_filters.py | 1 + 3 files changed, 9 insertions(+) diff --git a/starboard/tools/testing/test_filter.py b/starboard/tools/testing/test_filter.py index 6c2af98cf5db..6a787b0e5e9f 100644 --- a/starboard/tools/testing/test_filter.py +++ b/starboard/tools/testing/test_filter.py @@ -19,6 +19,13 @@ FILTER_ALL = 'FILTER_ALL' DISABLE_TESTING = 'DISABLE_TESTING' +EVERGREEN_ONLY_TESTS = { + 'elf_loader_test': {FILTER_ALL}, + 'installation_manager_test': {FILTER_ALL}, + 'reset_evergreen_update_test': {FILTER_ALL}, + 'slot_management_test': {FILTER_ALL}, +} + class TestFilter(object): """Container for data used to filter out a unit test. diff --git a/starboard/win/win32/test_filters.py b/starboard/win/win32/test_filters.py index cf8621fb81b0..98be41f5a04f 100644 --- a/starboard/win/win32/test_filters.py +++ b/starboard/win/win32/test_filters.py @@ -87,6 +87,7 @@ def GetTestFilters(self): return [test_filter.DISABLE_TESTING] else: filters = super().GetTestFilters() + _FILTERED_TESTS.update(test_filter.EVERGREEN_ONLY_TESTS) for target, tests in _FILTERED_TESTS.items(): filters.extend(test_filter.TestFilter(target, test) for test in tests) if os.environ.get('EXPERIMENTAL_CI', '0') == '1': diff --git a/starboard/xb1/test_filters.py b/starboard/xb1/test_filters.py index e545d5b15e0c..6bb45b5a1c21 100644 --- a/starboard/xb1/test_filters.py +++ b/starboard/xb1/test_filters.py @@ -59,6 +59,7 @@ def GetTestFilters(self): return [test_filter.DISABLE_TESTING] filters = super().GetTestFilters() + _FILTERED_TESTS.update(test_filter.EVERGREEN_ONLY_TESTS) for target, tests in _FILTERED_TESTS.items(): filters.extend(test_filter.TestFilter(target, test) for test in tests) return filters From 1418d390c5f4ba572692ea958280e7ff132410a2 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 7 Nov 2023 14:57:03 -0800 Subject: [PATCH 137/140] Cherry pick PR #1918: Build evergreen specific test targets only for non-windows platforms (#1924) Refer to the original PR: https://github.com/youtube/cobalt/pull/1918 Reverts #1873 partially Internal failures have been fixed. Note: For disabling evergreen specific tests for all non-evergreen platforms b/309493306 needs to be fixed. As a step in the right direction we are just disabling building disabling evergreen specific tests for windows platforms at the moment. b/302008891 Co-authored-by: Niranjan Yardi --- starboard/elf_loader/BUILD.gn | 52 ++++++++++---------- starboard/loader_app/BUILD.gn | 90 +++++++++++++++++++---------------- 2 files changed, 77 insertions(+), 65 deletions(-) diff --git a/starboard/elf_loader/BUILD.gn b/starboard/elf_loader/BUILD.gn index bca4126edd2b..64149322a481 100644 --- a/starboard/elf_loader/BUILD.gn +++ b/starboard/elf_loader/BUILD.gn @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import("//starboard/build/config/os_definitions.gni") + _elf_loader_sources = [ "dynamic_section.cc", "dynamic_section.h", @@ -88,7 +90,7 @@ if (sb_is_evergreen_compatible) { } } -if (current_toolchain == starboard_toolchain) { +if (current_toolchain == starboard_toolchain && !is_host_win) { target(starboard_level_final_executable_type, "elf_loader_sandbox") { data_deps = [ "//third_party/icu:icudata" ] if (cobalt_font_package == "empty") { @@ -152,31 +154,33 @@ if (sb_is_evergreen_compatible) { } } -target(gtest_target_type, "elf_loader_test") { - testonly = true - sources = [ "//starboard/common/test_main.cc" ] - deps = [ - "//starboard", - "//testing/gmock", - "//testing/gtest", - ] - - if (target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" || - target_cpu == "arm64") { - sources += [ - "dynamic_section_test.cc", - "elf_header_test.cc", - "elf_loader_test.cc", - "lz4_file_impl_test.cc", - "program_table_test.cc", - "relocations_test.cc", - ] - deps += [ - ":copy_elf_loader_testdata", - ":elf_loader", +if (!is_host_win) { + target(gtest_target_type, "elf_loader_test") { + testonly = true + sources = [ "//starboard/common/test_main.cc" ] + deps = [ + "//starboard", + "//testing/gmock", + "//testing/gtest", ] - data_deps = [ ":copy_elf_loader_testdata" ] + if (target_cpu == "x86" || target_cpu == "x64" || target_cpu == "arm" || + target_cpu == "arm64") { + sources += [ + "dynamic_section_test.cc", + "elf_header_test.cc", + "elf_loader_test.cc", + "lz4_file_impl_test.cc", + "program_table_test.cc", + "relocations_test.cc", + ] + deps += [ + ":copy_elf_loader_testdata", + ":elf_loader", + ] + + data_deps = [ ":copy_elf_loader_testdata" ] + } } } diff --git a/starboard/loader_app/BUILD.gn b/starboard/loader_app/BUILD.gn index e0384a1797c9..1e3ec6ed1fe5 100644 --- a/starboard/loader_app/BUILD.gn +++ b/starboard/loader_app/BUILD.gn @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +import("//starboard/build/config/os_definitions.gni") + _common_loader_app_sources = [ "loader_app.cc", "loader_app_switches.cc", @@ -247,20 +249,22 @@ static_library("installation_manager") { ] } -target(gtest_target_type, "installation_manager_test") { - testonly = true - sources = [ - "//starboard/common/test_main.cc", - "installation_manager_test.cc", - "pending_restart_test.cc", - ] - deps = [ - ":installation_manager", - ":installation_store_proto", - ":pending_restart", - "//testing/gmock", - "//testing/gtest", - ] +if (!is_host_win) { + target(gtest_target_type, "installation_manager_test") { + testonly = true + sources = [ + "//starboard/common/test_main.cc", + "installation_manager_test.cc", + "pending_restart_test.cc", + ] + deps = [ + ":installation_manager", + ":installation_store_proto", + ":pending_restart", + "//testing/gmock", + "//testing/gtest", + ] + } } static_library("slot_management") { @@ -285,22 +289,24 @@ static_library("slot_management") { } } -target(gtest_target_type, "slot_management_test") { - testonly = true - sources = [ - "//starboard/common/test_main.cc", - "slot_management_test.cc", - ] - deps = [ - ":app_key_files", - ":drain_file", - ":installation_manager", - ":installation_store_proto", - ":slot_management", - "//starboard/elf_loader:sabi_string", - "//testing/gmock", - "//testing/gtest", - ] +if (!is_host_win) { + target(gtest_target_type, "slot_management_test") { + testonly = true + sources = [ + "//starboard/common/test_main.cc", + "slot_management_test.cc", + ] + deps = [ + ":app_key_files", + ":drain_file", + ":installation_manager", + ":installation_store_proto", + ":slot_management", + "//starboard/elf_loader:sabi_string", + "//testing/gmock", + "//testing/gtest", + ] + } } static_library("pending_restart") { @@ -332,15 +338,17 @@ static_library("reset_evergreen_update") { deps = [ "//starboard" ] } -target(gtest_target_type, "reset_evergreen_update_test") { - testonly = true - sources = [ - "//starboard/common/test_main.cc", - "reset_evergreen_update_test.cc", - ] - deps = [ - ":reset_evergreen_update", - "//testing/gmock", - "//testing/gtest", - ] +if (!is_host_win) { + target(gtest_target_type, "reset_evergreen_update_test") { + testonly = true + sources = [ + "//starboard/common/test_main.cc", + "reset_evergreen_update_test.cc", + ] + deps = [ + ":reset_evergreen_update", + "//testing/gmock", + "//testing/gtest", + ] + } } From cd815d7cee7c89fdd7cf47269cc44d1f46be94d8 Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Tue, 7 Nov 2023 17:39:37 -0800 Subject: [PATCH 138/140] Cherry pick PR #1935: [Android] Log HDCP Level (#1938) Refer to the original PR: https://github.com/youtube/cobalt/pull/1935 As 4k drm requires hdcp 2.2, we log the hdcp level for debugging. b/307430294 Co-authored-by: Bo-Rong Chen --- .../java/dev/cobalt/media/MediaDrmBridge.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java index d4a12a5e7a9a..190a160a50f8 100644 --- a/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java +++ b/starboard/android/apk/app/src/main/java/dev/cobalt/media/MediaDrmBridge.java @@ -373,6 +373,11 @@ private MediaDrmBridge(String keySystem, UUID schemeUUID, long nativeMediaDrmBri mSchemeUUID = schemeUUID; mMediaDrm = new MediaDrm(schemeUUID); + // Get info of hdcp connection + if (Build.VERSION.SDK_INT >= 29) { + getConnectedHdcpLevelInfoV29(mMediaDrm); + } + mNativeMediaDrmBridge = nativeMediaDrmBridge; if (!isNativeMediaDrmBridgeValid()) { throw new IllegalArgumentException( @@ -738,6 +743,40 @@ private void closeMediaDrmV28(MediaDrm mediaDrm) { mediaDrm.close(); } + @RequiresApi(29) + private void getConnectedHdcpLevelInfoV29(MediaDrm mediaDrm) { + int hdcpLevel = mediaDrm.getConnectedHdcpLevel(); + switch (hdcpLevel) { + case MediaDrm.HDCP_V1: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_V1."); + break; + case MediaDrm.HDCP_V2: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2."); + break; + case MediaDrm.HDCP_V2_1: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2_1."); + break; + case MediaDrm.HDCP_V2_2: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2_2."); + break; + case MediaDrm.HDCP_V2_3: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_V2_3."); + break; + case MediaDrm.HDCP_NONE: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_NONE."); + break; + case MediaDrm.HDCP_NO_DIGITAL_OUTPUT: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_NO_DIGITAL_OUTPUT."); + break; + case MediaDrm.HDCP_LEVEL_UNKNOWN: + Log.i(TAG, "MediaDrm HDCP Level is HDCP_LEVEL_UNKNOWN."); + break; + default: + Log.i(TAG, String.format("Unknown MediaDrm HDCP level %d.", hdcpLevel)); + break; + } + } + private boolean isNativeMediaDrmBridgeValid() { return mNativeMediaDrmBridge != INVALID_NATIVE_MEDIA_DRM_BRIDGE; } From 9e2389acbd0ff1e5cd2be09cf6eba93e4b2af55c Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:15:36 -0800 Subject: [PATCH 139/140] Cherry pick PR #915: [XB1] Add support for extended interface of AbstractLauncher. (#1826) Refer to the original PR: https://github.com/youtube/cobalt/pull/915 [b/286272327](https://partnerissuetracker.corp.google.com/issues/286272327) Co-authored-by: v-kryachko --- starboard/tools/abstract_launcher.py | 78 ++++++++++++++++------ starboard/tools/testing/test_runner.py | 91 ++++++++++++++++++-------- starboard/xb1/launcher.py | 7 +- starboard/xb1/tools/xb1_launcher.py | 25 +++++-- 4 files changed, 141 insertions(+), 60 deletions(-) diff --git a/starboard/tools/abstract_launcher.py b/starboard/tools/abstract_launcher.py index 99280393ec1d..d57589e1fff8 100644 --- a/starboard/tools/abstract_launcher.py +++ b/starboard/tools/abstract_launcher.py @@ -18,6 +18,7 @@ import abc import os import sys +from enum import IntEnum from starboard.tools import build from starboard.tools import paths @@ -78,7 +79,7 @@ def LauncherFactory(platform_name, RuntimeError: The platform does not exist, or there is no project root. """ - # Creates launcher for provided platform if the platform has a valid port + # Creates launcher for provided platform if the platform has a valid port. launcher_module = _GetLauncherForPlatform(platform_name) if not launcher_module: @@ -97,8 +98,51 @@ def LauncherFactory(platform_name, **kwargs) +class TargetStatus(IntEnum): + """Represents status of the target run and its return code availability.""" + + # Target exited normally. Return code is available. + OK = 0 + + # Target exited normally. Return code is not available. + NA = 1 + + # Target crashed. + CRASH = 2 + + # Target not started. + NOT_STARTED = 3 + + @classmethod + def ToString(cls, status): + return [ + "SUCCEEDED", "SUCCEEDED", "FAILED (CRASHED)", "FAILED (NOT STARTED)" + ][status] + + class AbstractLauncher(object): - """Class that specifies all required behavior for Cobalt app launchers.""" + """ + Class that specifies all required behavior for Cobalt app launchers. + The following is definition of extended interface. Implementation is optional. + Functions: + - InitDevice: a function to be called before any other performance + on the target device. + - RebootDevice: a function, used to reboot the target device. + - Run2: a function to run a target on device. Must be implemented + to support extended interface. The target must have been deployed + on the device, if required. + Returns: + a tuple of ReturnCodeStatus and, target return code if available, + else 0. + - Deploy: creates a package (if required) and deploys it on the target + device. + - CheckPackageIsDeployed: a function, which returns True, if the target + is deployed on the device, False otherwise. Must be implemented, + if Deploy is implemented. + - Kill: request the target OS to kill the target process. Must be + implemented. + - SendStop: sends stop signal to the launcher's executable. + """ __metaclass__ = abc.ABCMeta @@ -143,22 +187,24 @@ def __init__(self, platform_name, target_name, config, device_id, **kwargs): self.test_result_xml_path = kwargs.get("test_result_xml_path", None) + def HasExtendedInterface(self) -> bool: + return hasattr(self, "Run2") + @abc.abstractmethod def Run(self): - """Runs the launcher's executable. + """Runs an underlying application. Supports launching target + executable on target device, or target executable directly. + Implementation is platform specific. Must be implemented in subclasses. Returns: - The return code from the launcher's executable. + The return code from the underlying application, if available, + else, 0 in case of normal completion of the underlying application, + otherwise 1. """ pass - @abc.abstractmethod - def Kill(self): - """Kills the launcher. Must be implemented in subclasses.""" - pass - @abc.abstractmethod def GetDeviceIp(self): """Gets the device IP. Must be implemented in subclasses.""" @@ -166,7 +212,7 @@ def GetDeviceIp(self): @abc.abstractmethod def GetDeviceOutputPath(self): - """Writable path where test targets can output files""" + """Writable path where test targets can output files.""" pass def SupportsSuspendResume(self): @@ -219,14 +265,6 @@ def SendFreeze(self): """ raise RuntimeError("Freeze not supported for this platform.") - def SendStop(self): - """sends stop signal to the launcher's executable. - - Raises: - RuntimeError: Stop signal not supported on platform. - """ - raise RuntimeError("Stop not supported for this platform.") - def SupportsDeepLink(self): return False @@ -290,7 +328,7 @@ def GetTargetPath(self): The default path returned by this method takes the form of: - "/path/to/out/_/target_name" + "/path/to/out/_/target_name". Returns: The path to an executable target. @@ -302,7 +340,7 @@ def GetInstallTargetPath(self): The default path returned by this method takes the form of: - "/path/to/out/_/install/target_name" + "/path/to/out/_/install/target_name". Returns: The path to an executable target. diff --git a/starboard/tools/testing/test_runner.py b/starboard/tools/testing/test_runner.py index 8ae6f499986a..0fb343a32e66 100755 --- a/starboard/tools/testing/test_runner.py +++ b/starboard/tools/testing/test_runner.py @@ -29,6 +29,7 @@ from six.moves import cStringIO as StringIO from starboard.build import clang from starboard.tools import abstract_launcher +from starboard.tools.abstract_launcher import TargetStatus from starboard.tools import build from starboard.tools import command_line from starboard.tools import paths @@ -160,14 +161,33 @@ class TestLauncher(object): communicate, and for the main thread to shut them down. """ - def __init__(self, launcher): + def __init__(self, launcher, skip_init): self.launcher = launcher - self.runner_thread = threading.Thread(target=self._Run) self.return_code_lock = threading.Lock() self.return_code = 1 + self.run_test = None + self.skip_init = skip_init def Start(self): + if self.launcher.HasExtendedInterface(): + if not self.skip_init: + if hasattr(self.launcher, "InitDevice"): + self.launcher.InitDevice() + if hasattr(self.launcher, "Deploy"): + assert hasattr(self.launcher, "CheckPackageIsDeployed") + if abstract_launcher.ARG_NOINSTALL not in self.launcher.launcher_args: + self.launcher.Deploy() + if not self.launcher.CheckPackageIsDeployed(): + raise IOError( + "The target application is not installed on the device.") + + self.run_test = self.launcher.Run2 + + else: + self.run_test = lambda: (TargetStatus.OK, self.launcher.Run()) + + self.runner_thread = threading.Thread(target=self._Run) self.runner_thread.start() def Kill(self): @@ -183,12 +203,12 @@ def Kill(self): def Join(self): self.runner_thread.join() - def _Run(self): - """Runs the launcher, and assigns a return code.""" - return_code = 1 + def _Run(self) -> None: + """Runs the launcher, and assigns a status and a return code.""" + return_code = TargetStatus.NOT_STARTED, 0 try: logging.info("Running launcher") - return_code = self.launcher.Run() + return_code = self.run_test() logging.info("Finished running launcher") except Exception: # pylint: disable=broad-except sys.stderr.write(f"Error while running {self.launcher.target_name}:\n") @@ -259,6 +279,7 @@ def __init__(self, self.xml_output_dir = xml_output_dir self.log_xml_results = log_xml_results self.threads = [] + self.is_initialized = False _EnsureBuildDirectoryExists(self.out_directory) _VerifyConfig(self._platform_config, @@ -273,6 +294,7 @@ def __init__(self, # If a particular test binary has been provided, configure only that one. logging.info("Getting test targets") + if specified_targets: self.test_targets = self._GetSpecifiedTestTargets(specified_targets) else: @@ -392,11 +414,11 @@ def _GetAllTestEnvVariables(self): env_variables[test] = test_env return env_variables - def _RunTest(self, - target_name, - test_name=None, - shard_index=None, - shard_count=None): + def RunTest(self, + target_name, + test_name=None, + shard_index=None, + shard_count=None): """Runs a specific target or test and collects the output. Args: @@ -497,7 +519,7 @@ def MakeLauncher(): logging.info("Launcher initialized") test_reader = TestLineReader(read_pipe) - test_launcher = TestLauncher(launcher) + test_launcher = TestLauncher(launcher, self.is_initialized) self.threads.append(test_launcher) self.threads.append(test_reader) @@ -524,6 +546,7 @@ def MakeLauncher(): # Wait for the launcher to exit then close the write pipe, which will # cause the reader to exit. test_launcher.Join() + self.is_initialized = True write_pipe.close() # Only after closing the write pipe, wait for the reader to exit. @@ -533,10 +556,10 @@ def MakeLauncher(): output = test_reader.GetLines() self.threads = [] - return self._CollectTestResults(output, target_name, - test_launcher.GetReturnCode()) + return (target_name, *self._CollectTestResults(output), + *test_launcher.GetReturnCode()) - def _CollectTestResults(self, results, target_name, return_code): + def _CollectTestResults(self, results): """Collects passing and failing tests for one test binary. Args: @@ -570,8 +593,7 @@ def _CollectTestResults(self, results, target_name, return_code): # Descriptions of all failed tests appear after this line failed_tests = self._CollectFailedTests(results[idx + 1:]) - return (target_name, total_count, passed_count, failed_count, failed_tests, - return_code) + return (total_count, passed_count, failed_count, failed_tests) def _CollectFailedTests(self, lines): """Collects the names of all failed tests. @@ -630,7 +652,8 @@ def _ProcessAllTestResults(self, results): passed_count = result_set[2] failed_count = result_set[3] failed_tests = result_set[4] - return_code = result_set[5] + return_code_status = result_set[5] + return_code = result_set[6] actual_failed_tests = [] flaky_failed_tests = [] filtered_tests = self._GetFilteredTestList(target_name) @@ -660,7 +683,7 @@ def _ProcessAllTestResults(self, results): for retry in range(_FLAKY_RETRY_LIMIT): # Sometimes the returned test "name" includes information about the # parameter that was passed to it. This needs to be stripped off. - retry_result = self._RunTest(target_name, test_case.split(",")[0]) + retry_result = self.RunTest(target_name, test_case.split(",")[0]) print() # Explicit print for empty formatting line. if retry_result[2] == 1: flaky_passed_tests.append(test_case) @@ -676,24 +699,26 @@ def _ProcessAllTestResults(self, results): else: logging.info("") # formatting newline. - test_status = "SUCCEEDED" + test_status = TargetStatus.ToString(return_code_status) all_flaky_tests_succeeded = initial_flaky_failed_count == len( flaky_passed_tests) and initial_flaky_failed_count != 0 # Always mark as FAILED if we have a non-zero return code, or failing # test. - if ((return_code != 0 and not all_flaky_tests_succeeded) or - actual_failed_count > 0 or flaky_failed_count > 0): + if ((return_code_status == TargetStatus.OK and return_code != 0 and + not all_flaky_tests_succeeded) or actual_failed_count > 0 or + flaky_failed_count > 0): error = True - test_status = "FAILED" failed_test_groups.append(target_name) # Be specific about the cause of failure if it was caused due to crash # upon exit. Normal Gtest failures have return_code = 1; test crashes # yield different return codes (e.g. segfault has return_code = 11). if (return_code != 1 and actual_failed_count == 0 and flaky_failed_count == 0): - test_status = "FAILED (CRASHED)" + test_status = "FAILED (ISSUE)" + else: + test_status = "FAILED" logging.info("%s: %s.", target_name, test_status) if return_code != 0 and run_count == 0 and filtered_count == 0: @@ -808,12 +833,12 @@ def RunAllTests(self): if run_action == ShardingTestConfig.RUN_FULL_TEST: logging.info("SHARD %d RUNS TEST %s (full)", self.shard_index, test_target) - results.append(self._RunTest(test_target)) + results.append(self.RunTest(test_target)) elif run_action == ShardingTestConfig.RUN_PARTIAL_TEST: logging.info("SHARD %d RUNS TEST %s (%d of %d)", self.shard_index, test_target, sub_shard_index + 1, sub_shard_count) results.append( - self._RunTest( + self.RunTest( test_target, shard_index=sub_shard_index, shard_count=sub_shard_count)) @@ -822,7 +847,7 @@ def RunAllTests(self): logging.info("SHARD %d SKIP TEST %s", self.shard_index, test_target) else: # Run all tests and cases serially. No sharding enabled. - results.append(self._RunTest(test_target)) + results.append(self.RunTest(test_target)) return self._ProcessAllTestResults(results) def GenerateCoverageReport(self): @@ -957,6 +982,7 @@ def main(): launcher_args.append(abstract_launcher.ARG_DRYRUN) logging.info("Initializing test runner") + runner = TestRunner(args.platform, args.config, args.loader_platform, args.loader_config, args.device_id, args.target_name, target_params, args.out_directory, @@ -991,7 +1017,16 @@ def Abort(signum, frame): return 1 if args.run: - run_success = runner.RunAllTests() + if isinstance(args.target_name, list) and len(args.target_name) == 1: + r = runner.RunTest(args.target_name[0]) + if r[5] == TargetStatus.OK: + return r[6] + elif r[5] == TargetStatus.NA: + return 0 + else: + return 1 + else: + run_success = runner.RunAllTests() runner.GenerateCoverageReport() diff --git a/starboard/xb1/launcher.py b/starboard/xb1/launcher.py index 9be3550152c0..583aa3b9b031 100644 --- a/starboard/xb1/launcher.py +++ b/starboard/xb1/launcher.py @@ -62,12 +62,7 @@ def Kill(self): # All other functions are automatically delegated using this function. def __getattr__(self, fname): - - def method(*args): - f = getattr(self.delegate, fname) - return f(*args) - - return method + return getattr(self.delegate, fname) def GetDeviceIp(self): """Gets the device IP. TODO: Implement.""" diff --git a/starboard/xb1/tools/xb1_launcher.py b/starboard/xb1/tools/xb1_launcher.py index 7f9a74baa886..b83a9e6984a7 100644 --- a/starboard/xb1/tools/xb1_launcher.py +++ b/starboard/xb1/tools/xb1_launcher.py @@ -80,6 +80,7 @@ from starboard.shared.win32 import mini_dump_printer from starboard.tools import abstract_launcher +from starboard.tools.abstract_launcher import TargetStatus from starboard.tools import net_args from starboard.tools import net_log from starboard.xb1.tools import packager @@ -498,16 +499,19 @@ def RunTest(self, appx_name: str): if hasattr(self, 'net_args_thread'): self.net_args_thread.join() + def InitDevice(self): + if not self._network_api.IsInDevMode(): + raise IOError('\n\n**** Please set the XBOX at ' + self._device_id + + ' to dev mode!!!! ****\n') + self.SignIn() + def Run(self): # Only upload and install Appx on the first run. if FirstRun(): if self._do_restart: self.RestartDevkit() - if not self._network_api.IsInDevMode(): - raise IOError('\n\n**** Please set the XBOX at ' + self._device_id + - ' to dev mode!!!! ****\n') - self.SignIn() + self.InitDevice() if self._do_deploy: self.Deploy() else: @@ -522,6 +526,13 @@ def Run(self): self._LogLn('Skipping running step.') return 0 + status, _ = self.Run2() + if status == TargetStatus.CRASH: + return 1 + else: + return 0 + + def Run2(self): try: self.Kill() # Kill existing running app. # While binary is running, extract the net log and stream it to @@ -557,8 +568,10 @@ def Run(self): self.Kill() self._LogLn('Finished running...') - crashed = self._DetectAndHandleAnyCrashes(self.target_name) - return crashed + if self._DetectAndHandleAnyCrashes(self.target_name): + return TargetStatus.CRASH, 0 + else: + return TargetStatus.NA, 0 def _DetectAndHandleAnyCrashes(self, target_name): crashes_detected = False From 8cf87146a5c5fe69f57a56cf45f6619c5873bf3e Mon Sep 17 00:00:00 2001 From: cobalt-github-releaser-bot <95661244+cobalt-github-releaser-bot@users.noreply.github.com> Date: Wed, 8 Nov 2023 10:30:13 -0800 Subject: [PATCH 140/140] Cherry pick PR #1925: Add TODO statements from previous PR feedback (#1937) Refer to the original PR: https://github.com/youtube/cobalt/pull/1925 Adding TODO statements from feedback in https://github.com/youtube/cobalt/pull/1918 b/309493306 Co-authored-by: Niranjan Yardi --- starboard/elf_loader/BUILD.gn | 2 ++ starboard/loader_app/BUILD.gn | 3 +++ 2 files changed, 5 insertions(+) diff --git a/starboard/elf_loader/BUILD.gn b/starboard/elf_loader/BUILD.gn index 64149322a481..184ccb26e061 100644 --- a/starboard/elf_loader/BUILD.gn +++ b/starboard/elf_loader/BUILD.gn @@ -90,6 +90,7 @@ if (sb_is_evergreen_compatible) { } } +# TODO: b/309493306 - Stop building evergreen targets for all non-evergreen platforms. if (current_toolchain == starboard_toolchain && !is_host_win) { target(starboard_level_final_executable_type, "elf_loader_sandbox") { data_deps = [ "//third_party/icu:icudata" ] @@ -154,6 +155,7 @@ if (sb_is_evergreen_compatible) { } } +# TODO: b/309493306 - Stop building evergreen targets for all non-evergreen platforms. if (!is_host_win) { target(gtest_target_type, "elf_loader_test") { testonly = true diff --git a/starboard/loader_app/BUILD.gn b/starboard/loader_app/BUILD.gn index 1e3ec6ed1fe5..053c0c231d22 100644 --- a/starboard/loader_app/BUILD.gn +++ b/starboard/loader_app/BUILD.gn @@ -249,6 +249,7 @@ static_library("installation_manager") { ] } +# TODO: b/309493306 - Stop building evergreen targets for all non-evergreen platforms. if (!is_host_win) { target(gtest_target_type, "installation_manager_test") { testonly = true @@ -289,6 +290,7 @@ static_library("slot_management") { } } +# TODO: b/309493306 - Stop building evergreen targets for all non-evergreen platforms. if (!is_host_win) { target(gtest_target_type, "slot_management_test") { testonly = true @@ -338,6 +340,7 @@ static_library("reset_evergreen_update") { deps = [ "//starboard" ] } +# TODO: b/309493306 - Stop building evergreen targets for all non-evergreen platforms. if (!is_host_win) { target(gtest_target_type, "reset_evergreen_update_test") { testonly = true