| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "media/base/android/media_player_bridge.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/android/jni_android.h" |
| #include "base/android/jni_string.h" |
| #include "base/android/scoped_java_ref.h" |
| #include "base/bind.h" |
| #include "base/check_op.h" |
| #include "base/cxx17_backports.h" |
| #include "base/metrics/histogram_macros.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_util.h" |
| #include "base/threading/thread_task_runner_handle.h" |
| #include "media/base/android/media_common_android.h" |
| #include "media/base/android/media_jni_headers/MediaPlayerBridge_jni.h" |
| #include "media/base/android/media_resource_getter.h" |
| #include "media/base/android/media_url_interceptor.h" |
| #include "media/base/timestamp_constants.h" |
| |
| using base::android::ConvertUTF8ToJavaString; |
| using base::android::JavaParamRef; |
| using base::android::JavaRef; |
| using base::android::ScopedJavaLocalRef; |
| |
| namespace media { |
| |
| namespace { |
| |
| enum UMAExitStatus { |
| UMA_EXIT_SUCCESS = 0, |
| UMA_EXIT_ERROR, |
| UMA_EXIT_STATUS_MAX = UMA_EXIT_ERROR, |
| }; |
| |
| const double kDefaultVolume = 1.0; |
| |
| const char kWatchTimeHistogram[] = "Media.Android.MediaPlayerWatchTime"; |
| |
| // These values are persisted to logs. Entries should not be renumbered and |
| // numeric values should never be reused. |
| enum class WatchTimeType { |
| kNonHls = 0, |
| kHlsAudioOnly = 1, |
| kHlsVideo = 2, |
| kMaxValue = kHlsVideo, |
| }; |
| |
| void RecordWatchTimeUMA(bool is_hls, bool has_video) { |
| WatchTimeType type = WatchTimeType::kNonHls; |
| if (is_hls) { |
| if (!has_video) { |
| type = WatchTimeType::kHlsAudioOnly; |
| } else { |
| type = WatchTimeType::kHlsVideo; |
| } |
| } |
| UMA_HISTOGRAM_ENUMERATION(kWatchTimeHistogram, type); |
| } |
| |
| } // namespace |
| |
| MediaPlayerBridge::MediaPlayerBridge( |
| const GURL& url, |
| const net::SiteForCookies& site_for_cookies, |
| const url::Origin& top_frame_origin, |
| const std::string& user_agent, |
| bool hide_url_log, |
| Client* client, |
| bool allow_credentials, |
| bool is_hls) |
| : prepared_(false), |
| playback_completed_(false), |
| pending_play_(false), |
| should_seek_on_prepare_(false), |
| url_(url), |
| site_for_cookies_(site_for_cookies), |
| top_frame_origin_(top_frame_origin), |
| pending_retrieve_cookies_(false), |
| should_prepare_on_retrieved_cookies_(false), |
| user_agent_(user_agent), |
| hide_url_log_(hide_url_log), |
| width_(0), |
| height_(0), |
| can_seek_forward_(true), |
| can_seek_backward_(true), |
| volume_(kDefaultVolume), |
| allow_credentials_(allow_credentials), |
| is_active_(false), |
| has_error_(false), |
| has_ever_started_(false), |
| is_hls_(is_hls), |
| watch_timer_(base::BindRepeating(&MediaPlayerBridge::OnWatchTimerTick, |
| base::Unretained(this)), |
| base::BindRepeating(&MediaPlayerBridge::GetCurrentTime, |
| base::Unretained(this))), |
| client_(client) { |
| listener_ = std::make_unique<MediaPlayerListener>( |
| base::ThreadTaskRunnerHandle::Get(), weak_factory_.GetWeakPtr()); |
| } |
| |
| MediaPlayerBridge::~MediaPlayerBridge() { |
| if (!j_media_player_bridge_.is_null()) { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| Java_MediaPlayerBridge_destroy(env, j_media_player_bridge_); |
| } |
| Release(); |
| |
| if (has_ever_started_) { |
| UMA_HISTOGRAM_ENUMERATION("Media.Android.MediaPlayerSuccess", |
| has_error_ ? UMA_EXIT_ERROR : UMA_EXIT_SUCCESS, |
| UMA_EXIT_STATUS_MAX + 1); |
| } |
| } |
| |
| void MediaPlayerBridge::Initialize() { |
| cookies_.clear(); |
| if (url_.SchemeIsBlob()) { |
| NOTREACHED(); |
| return; |
| } |
| |
| if (allow_credentials_ && !url_.SchemeIsFile()) { |
| media::MediaResourceGetter* resource_getter = |
| client_->GetMediaResourceGetter(); |
| |
| pending_retrieve_cookies_ = true; |
| resource_getter->GetCookies( |
| url_, site_for_cookies_, top_frame_origin_, |
| base::BindOnce(&MediaPlayerBridge::OnCookiesRetrieved, |
| weak_factory_.GetWeakPtr())); |
| } |
| } |
| |
| void MediaPlayerBridge::CreateJavaMediaPlayerBridge() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| j_media_player_bridge_.Reset(Java_MediaPlayerBridge_create( |
| env, reinterpret_cast<intptr_t>(this))); |
| |
| UpdateVolumeInternal(); |
| |
| AttachListener(j_media_player_bridge_); |
| } |
| |
| void MediaPlayerBridge::PropagateDuration(base::TimeDelta duration) { |
| duration_ = duration; |
| client_->OnMediaDurationChanged(duration_); |
| } |
| |
| void MediaPlayerBridge::SetVideoSurface(gl::ScopedJavaSurface surface) { |
| surface_ = std::move(surface); |
| |
| if (j_media_player_bridge_.is_null()) |
| return; |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| Java_MediaPlayerBridge_setSurface(env, j_media_player_bridge_, |
| surface_.j_surface()); |
| } |
| |
| void MediaPlayerBridge::SetPlaybackRate(double playback_rate) { |
| if (j_media_player_bridge_.is_null()) |
| return; |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| Java_MediaPlayerBridge_setPlaybackRate(env, j_media_player_bridge_, |
| playback_rate); |
| } |
| |
| void MediaPlayerBridge::Prepare() { |
| DCHECK(j_media_player_bridge_.is_null()); |
| |
| if (url_.SchemeIsBlob()) { |
| NOTREACHED(); |
| return; |
| } |
| |
| CreateJavaMediaPlayerBridge(); |
| |
| if (url_.SchemeIsFileSystem()) { |
| client_->GetMediaResourceGetter()->GetPlatformPathFromURL( |
| url_, base::BindOnce(&MediaPlayerBridge::SetDataSource, |
| weak_factory_.GetWeakPtr())); |
| return; |
| } |
| |
| SetDataSource(url_.spec()); |
| } |
| |
| void MediaPlayerBridge::SetDataSource(const std::string& url) { |
| if (j_media_player_bridge_.is_null()) |
| return; |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| int fd; |
| int64_t offset; |
| int64_t size; |
| if (InterceptMediaUrl(url, &fd, &offset, &size)) { |
| if (!Java_MediaPlayerBridge_setDataSourceFromFd(env, j_media_player_bridge_, |
| fd, offset, size)) { |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| return; |
| } |
| |
| if (!Java_MediaPlayerBridge_prepareAsync(env, j_media_player_bridge_)) |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| |
| return; |
| } |
| |
| // Create a Java String for the URL. |
| ScopedJavaLocalRef<jstring> j_url_string = ConvertUTF8ToJavaString(env, url); |
| |
| const std::string data_uri_prefix("data:"); |
| if (base::StartsWith(url, data_uri_prefix, base::CompareCase::SENSITIVE)) { |
| if (!Java_MediaPlayerBridge_setDataUriDataSource( |
| env, j_media_player_bridge_, j_url_string)) { |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| } |
| return; |
| } |
| |
| // Cookies may not have been retrieved yet, delay prepare until they are |
| // retrieved. |
| if (pending_retrieve_cookies_) { |
| should_prepare_on_retrieved_cookies_ = true; |
| return; |
| } |
| SetDataSourceInternal(); |
| } |
| |
| void MediaPlayerBridge::SetDataSourceInternal() { |
| DCHECK(!pending_retrieve_cookies_); |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| ScopedJavaLocalRef<jstring> j_cookies = |
| ConvertUTF8ToJavaString(env, cookies_); |
| ScopedJavaLocalRef<jstring> j_user_agent = |
| ConvertUTF8ToJavaString(env, user_agent_); |
| ScopedJavaLocalRef<jstring> j_url_string = |
| ConvertUTF8ToJavaString(env, url_.spec()); |
| |
| if (!Java_MediaPlayerBridge_setDataSource(env, j_media_player_bridge_, |
| j_url_string, j_cookies, |
| j_user_agent, hide_url_log_)) { |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| return; |
| } |
| |
| if (!Java_MediaPlayerBridge_prepareAsync(env, j_media_player_bridge_)) |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| } |
| |
| bool MediaPlayerBridge::InterceptMediaUrl(const std::string& url, |
| int* fd, |
| int64_t* offset, |
| int64_t* size) { |
| // Sentinel value to check whether the output arguments have been set. |
| const int kUnsetValue = -1; |
| |
| *fd = kUnsetValue; |
| *offset = kUnsetValue; |
| *size = kUnsetValue; |
| media::MediaUrlInterceptor* url_interceptor = |
| client_->GetMediaUrlInterceptor(); |
| if (url_interceptor && url_interceptor->Intercept(url, fd, offset, size)) { |
| DCHECK_NE(kUnsetValue, *fd); |
| DCHECK_NE(kUnsetValue, *offset); |
| DCHECK_NE(kUnsetValue, *size); |
| return true; |
| } |
| return false; |
| } |
| |
| void MediaPlayerBridge::OnDidSetDataUriDataSource( |
| JNIEnv* env, |
| const JavaParamRef<jobject>& obj, |
| jboolean success) { |
| if (!success) { |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| return; |
| } |
| |
| if (!Java_MediaPlayerBridge_prepareAsync(env, j_media_player_bridge_)) |
| OnMediaError(MEDIA_ERROR_FORMAT); |
| } |
| |
| void MediaPlayerBridge::OnCookiesRetrieved(const std::string& cookies) { |
| cookies_ = cookies; |
| pending_retrieve_cookies_ = false; |
| client_->GetMediaResourceGetter()->GetAuthCredentials( |
| url_, base::BindOnce(&MediaPlayerBridge::OnAuthCredentialsRetrieved, |
| weak_factory_.GetWeakPtr())); |
| |
| if (should_prepare_on_retrieved_cookies_) { |
| SetDataSourceInternal(); |
| should_prepare_on_retrieved_cookies_ = false; |
| } |
| } |
| |
| void MediaPlayerBridge::OnAuthCredentialsRetrieved( |
| const std::u16string& username, |
| const std::u16string& password) { |
| GURL::ReplacementsW replacements; |
| if (!username.empty()) { |
| replacements.SetUsernameStr(username); |
| if (!password.empty()) |
| replacements.SetPasswordStr(password); |
| url_ = url_.ReplaceComponents(replacements); |
| } |
| } |
| |
| void MediaPlayerBridge::Start() { |
| // A second Start() call after an error is considered another attempt for UMA |
| // and causes UMA reporting. |
| if (has_ever_started_ && has_error_) { |
| UMA_HISTOGRAM_ENUMERATION("Media.Android.MediaPlayerSuccess", |
| UMA_EXIT_ERROR, UMA_EXIT_STATUS_MAX + 1); |
| } |
| |
| has_ever_started_ = true; |
| has_error_ = false; |
| is_active_ = true; |
| |
| if (j_media_player_bridge_.is_null()) { |
| pending_play_ = true; |
| Prepare(); |
| } else { |
| if (prepared_) |
| StartInternal(); |
| else |
| pending_play_ = true; |
| } |
| } |
| |
| void MediaPlayerBridge::Pause() { |
| if (j_media_player_bridge_.is_null()) { |
| pending_play_ = false; |
| } else { |
| if (prepared_ && IsPlaying()) |
| PauseInternal(); |
| else |
| pending_play_ = false; |
| } |
| |
| is_active_ = false; |
| } |
| |
| bool MediaPlayerBridge::IsPlaying() { |
| DCHECK(prepared_); |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| jboolean result = |
| Java_MediaPlayerBridge_isPlaying(env, j_media_player_bridge_); |
| return result; |
| } |
| |
| void MediaPlayerBridge::SeekTo(base::TimeDelta timestamp) { |
| // Record the time to seek when OnMediaPrepared() is called. |
| pending_seek_ = timestamp; |
| should_seek_on_prepare_ = true; |
| |
| if (prepared_) |
| SeekInternal(timestamp); |
| } |
| |
| base::TimeDelta MediaPlayerBridge::GetCurrentTime() { |
| if (!prepared_) |
| return pending_seek_; |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| return base::Milliseconds( |
| Java_MediaPlayerBridge_getCurrentPosition(env, j_media_player_bridge_)); |
| } |
| |
| base::TimeDelta MediaPlayerBridge::GetDuration() { |
| DCHECK(prepared_); |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| const int duration_ms = |
| Java_MediaPlayerBridge_getDuration(env, j_media_player_bridge_); |
| return duration_ms < 0 ? media::kInfiniteDuration |
| : base::Milliseconds(duration_ms); |
| } |
| |
| void MediaPlayerBridge::Release() { |
| watch_timer_.Stop(); |
| is_active_ = false; |
| |
| if (j_media_player_bridge_.is_null()) |
| return; |
| |
| if (prepared_) { |
| pending_seek_ = GetCurrentTime(); |
| should_seek_on_prepare_ = true; |
| } |
| |
| prepared_ = false; |
| pending_play_ = false; |
| SetVideoSurface(gl::ScopedJavaSurface()); |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_MediaPlayerBridge_release(env, j_media_player_bridge_); |
| j_media_player_bridge_.Reset(); |
| DetachListener(); |
| } |
| |
| void MediaPlayerBridge::SetVolume(double volume) { |
| volume_ = base::clamp(volume, 0.0, 1.0); |
| UpdateVolumeInternal(); |
| } |
| |
| void MediaPlayerBridge::UpdateVolumeInternal() { |
| if (j_media_player_bridge_.is_null()) { |
| return; |
| } |
| |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| Java_MediaPlayerBridge_setVolume(env, j_media_player_bridge_, volume_); |
| } |
| |
| void MediaPlayerBridge::OnVideoSizeChanged(int width, int height) { |
| width_ = width; |
| height_ = height; |
| client_->OnVideoSizeChanged(width, height); |
| } |
| |
| void MediaPlayerBridge::OnMediaError(int error_type) { |
| // Gather errors for UMA only in the active state. |
| // The MEDIA_ERROR_INVALID_CODE is reported by MediaPlayerListener.java in |
| // the situations that are considered normal, and is ignored by upper level. |
| if (is_active_ && error_type != MEDIA_ERROR_INVALID_CODE) |
| has_error_ = true; |
| |
| // Do not propagate MEDIA_ERROR_SERVER_DIED. If it happens in the active state |
| // we want the playback to stall. It can be recovered by pressing the Play |
| // button again. |
| if (error_type == MEDIA_ERROR_SERVER_DIED) |
| error_type = MEDIA_ERROR_INVALID_CODE; |
| |
| client_->OnError(error_type); |
| } |
| |
| void MediaPlayerBridge::OnPlaybackComplete() { |
| if (!playback_completed_) { |
| playback_completed_ = true; |
| client_->OnPlaybackComplete(); |
| } |
| } |
| |
| void MediaPlayerBridge::OnMediaPrepared() { |
| if (j_media_player_bridge_.is_null()) |
| return; |
| |
| prepared_ = true; |
| PropagateDuration(GetDuration()); |
| |
| UpdateAllowedOperations(); |
| |
| // If media player was recovered from a saved state, consume all the pending |
| // events. |
| if (should_seek_on_prepare_) { |
| SeekInternal(pending_seek_); |
| pending_seek_ = base::Milliseconds(0); |
| should_seek_on_prepare_ = false; |
| } |
| |
| if (!surface_.IsEmpty()) |
| SetVideoSurface(std::move(surface_)); |
| |
| if (pending_play_) { |
| StartInternal(); |
| pending_play_ = false; |
| } |
| } |
| |
| ScopedJavaLocalRef<jobject> MediaPlayerBridge::GetAllowedOperations() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| return Java_MediaPlayerBridge_getAllowedOperations(env, |
| j_media_player_bridge_); |
| } |
| |
| void MediaPlayerBridge::AttachListener(const JavaRef<jobject>& j_media_player) { |
| listener_->CreateMediaPlayerListener(j_media_player); |
| } |
| |
| void MediaPlayerBridge::DetachListener() { |
| listener_->ReleaseMediaPlayerListenerResources(); |
| } |
| |
| base::WeakPtr<MediaPlayerBridge> MediaPlayerBridge::WeakPtrForUIThread() { |
| return weak_factory_.GetWeakPtr(); |
| } |
| |
| void MediaPlayerBridge::UpdateAllowedOperations() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| |
| ScopedJavaLocalRef<jobject> allowedOperations = GetAllowedOperations(); |
| |
| can_seek_forward_ = |
| Java_AllowedOperations_canSeekForward(env, allowedOperations); |
| can_seek_backward_ = |
| Java_AllowedOperations_canSeekBackward(env, allowedOperations); |
| } |
| |
| void MediaPlayerBridge::StartInternal() { |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_MediaPlayerBridge_start(env, j_media_player_bridge_); |
| watch_timer_.Start(); |
| } |
| |
| void MediaPlayerBridge::PauseInternal() { |
| watch_timer_.Stop(); |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| Java_MediaPlayerBridge_pause(env, j_media_player_bridge_); |
| } |
| |
| void MediaPlayerBridge::SeekInternal(base::TimeDelta time) { |
| base::TimeDelta current_time = GetCurrentTime(); |
| |
| // Seeking on content like live streams may cause the media player to |
| // get stuck in an error state. |
| if (time < current_time && !can_seek_backward_) |
| return; |
| |
| if (time >= current_time && !can_seek_forward_) |
| return; |
| |
| if (time > duration_) |
| time = duration_; |
| |
| // Seeking to an invalid position may cause media player to stuck in an |
| // error state. |
| if (time < base::TimeDelta()) { |
| DCHECK_EQ(-1.0, time.InMillisecondsF()); |
| return; |
| } |
| |
| playback_completed_ = false; |
| |
| // Note: we do not want to count changes in media time due to seeks as watch |
| // time, but tracking pending seeks is not completely trivial. Instead seeks |
| // larger than kWatchTimeReportingInterval * 2 will be discarded by the sanity |
| // checks and shorter seeks will be counted. |
| JNIEnv* env = base::android::AttachCurrentThread(); |
| CHECK(env); |
| int time_msec = static_cast<int>(time.InMilliseconds()); |
| Java_MediaPlayerBridge_seekTo(env, j_media_player_bridge_, time_msec); |
| } |
| |
| GURL MediaPlayerBridge::GetUrl() { |
| return url_; |
| } |
| |
| const net::SiteForCookies& MediaPlayerBridge::GetSiteForCookies() { |
| return site_for_cookies_; |
| } |
| |
| void MediaPlayerBridge::OnWatchTimerTick() { |
| RecordWatchTimeUMA(is_hls_, height_ > 0); |
| } |
| |
| } // namespace media |