| 1 | /* |
| 2 | * Copyright (C) 2007, 2009 Apple Inc. All rights reserved. |
| 3 | * Copyright (C) 2007 Collabora Ltd. All rights reserved. |
| 4 | * Copyright (C) 2007 Alp Toker <alp@atoker.com> |
| 5 | * Copyright (C) 2009 Gustavo Noronha Silva <gns@gnome.org> |
| 6 | * Copyright (C) 2009, 2010, 2011, 2012, 2013, 2015, 2016 Igalia S.L |
| 7 | * Copyright (C) 2014 Cable Television Laboratories, Inc. |
| 8 | * Copyright (C) 2015, 2016 Metrological Group B.V. |
| 9 | * |
| 10 | * This library is free software; you can redistribute it and/or |
| 11 | * modify it under the terms of the GNU Library General Public |
| 12 | * License as published by the Free Software Foundation; either |
| 13 | * version 2 of the License, or (at your option) any later version. |
| 14 | * |
| 15 | * This library is distributed in the hope that it will be useful, |
| 16 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| 17 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
| 18 | * Library General Public License for more details. |
| 19 | * |
| 20 | * You should have received a copy of the GNU Library General Public License |
| 21 | * aint with this library; see the file COPYING.LIB. If not, write to |
| 22 | * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, |
| 23 | * Boston, MA 02110-1301, USA. |
| 24 | */ |
| 25 | |
| 26 | #include "config.h" |
| 27 | #include "MediaPlayerPrivateGStreamer.h" |
| 28 | |
| 29 | #if ENABLE(VIDEO) && USE(GSTREAMER) |
| 30 | |
| 31 | #include "GStreamerCommon.h" |
| 32 | #include "GStreamerRegistryScanner.h" |
| 33 | #include "HTTPHeaderNames.h" |
| 34 | #include "MIMETypeRegistry.h" |
| 35 | #include "MediaPlayer.h" |
| 36 | #include "MediaPlayerRequestInstallMissingPluginsCallback.h" |
| 37 | #include "NotImplemented.h" |
| 38 | #include "SecurityOrigin.h" |
| 39 | #include "TimeRanges.h" |
| 40 | #include "WebKitWebSourceGStreamer.h" |
| 41 | #include <glib.h> |
| 42 | #include <gst/gst.h> |
| 43 | #include <gst/pbutils/missing-plugins.h> |
| 44 | #include <limits> |
| 45 | #include <wtf/FileSystem.h> |
| 46 | #include <wtf/HexNumber.h> |
| 47 | #include <wtf/MediaTime.h> |
| 48 | #include <wtf/NeverDestroyed.h> |
| 49 | #include <wtf/StringPrintStream.h> |
| 50 | #include <wtf/URL.h> |
| 51 | #include <wtf/WallTime.h> |
| 52 | #include <wtf/glib/GUniquePtr.h> |
| 53 | #include <wtf/glib/RunLoopSourcePriority.h> |
| 54 | #include <wtf/text/CString.h> |
| 55 | #include <wtf/text/StringConcatenateNumbers.h> |
| 56 | |
| 57 | #if ENABLE(MEDIA_STREAM) && GST_CHECK_VERSION(1, 10, 0) |
| 58 | #include "GStreamerMediaStreamSource.h" |
| 59 | #endif |
| 60 | |
| 61 | #if ENABLE(VIDEO_TRACK) |
| 62 | #include "AudioTrackPrivateGStreamer.h" |
| 63 | #include "InbandMetadataTextTrackPrivateGStreamer.h" |
| 64 | #include "InbandTextTrackPrivateGStreamer.h" |
| 65 | #include "TextCombinerGStreamer.h" |
| 66 | #include "TextSinkGStreamer.h" |
| 67 | #include "VideoTrackPrivateGStreamer.h" |
| 68 | #endif |
| 69 | |
| 70 | #if ENABLE(VIDEO_TRACK) && USE(GSTREAMER_MPEGTS) |
| 71 | #define GST_USE_UNSTABLE_API |
| 72 | #include <gst/mpegts/mpegts.h> |
| 73 | #undef GST_USE_UNSTABLE_API |
| 74 | #endif |
| 75 | #include <gst/audio/streamvolume.h> |
| 76 | |
| 77 | #if ENABLE(MEDIA_SOURCE) |
| 78 | #include "MediaSource.h" |
| 79 | #include "WebKitMediaSourceGStreamer.h" |
| 80 | #endif |
| 81 | |
| 82 | #if ENABLE(WEB_AUDIO) |
| 83 | #include "AudioSourceProviderGStreamer.h" |
| 84 | #endif |
| 85 | |
| 86 | GST_DEBUG_CATEGORY_EXTERN(webkit_media_player_debug); |
| 87 | #define GST_CAT_DEFAULT webkit_media_player_debug |
| 88 | |
| 89 | |
| 90 | namespace WebCore { |
| 91 | using namespace std; |
| 92 | |
| 93 | static void busMessageCallback(GstBus*, GstMessage* message, MediaPlayerPrivateGStreamer* player) |
| 94 | { |
| 95 | player->handleMessage(message); |
| 96 | } |
| 97 | |
| 98 | void MediaPlayerPrivateGStreamer::setAudioStreamPropertiesCallback(MediaPlayerPrivateGStreamer* player, GObject* object) |
| 99 | { |
| 100 | player->setAudioStreamProperties(object); |
| 101 | } |
| 102 | |
| 103 | void MediaPlayerPrivateGStreamer::setAudioStreamProperties(GObject* object) |
| 104 | { |
| 105 | if (g_strcmp0(G_OBJECT_TYPE_NAME(object), "GstPulseSink" )) |
| 106 | return; |
| 107 | |
| 108 | const char* role = m_player->client().mediaPlayerIsVideo() ? "video" : "music" ; |
| 109 | GstStructure* structure = gst_structure_new("stream-properties" , "media.role" , G_TYPE_STRING, role, nullptr); |
| 110 | g_object_set(object, "stream-properties" , structure, nullptr); |
| 111 | gst_structure_free(structure); |
| 112 | GUniquePtr<gchar> elementName(gst_element_get_name(GST_ELEMENT(object))); |
| 113 | GST_DEBUG_OBJECT(pipeline(), "Set media.role as %s at %s" , role, elementName.get()); |
| 114 | } |
| 115 | |
| 116 | void MediaPlayerPrivateGStreamer::registerMediaEngine(MediaEngineRegistrar registrar) |
| 117 | { |
| 118 | MediaPlayerPrivateGStreamerBase::initializeDebugCategory(); |
| 119 | if (isAvailable()) { |
| 120 | registrar([](MediaPlayer* player) { return std::make_unique<MediaPlayerPrivateGStreamer>(player); }, |
| 121 | getSupportedTypes, supportsType, nullptr, nullptr, nullptr, supportsKeySystem); |
| 122 | } |
| 123 | } |
| 124 | |
| 125 | bool MediaPlayerPrivateGStreamer::isAvailable() |
| 126 | { |
| 127 | if (!initializeGStreamerAndRegisterWebKitElements()) |
| 128 | return false; |
| 129 | |
| 130 | GRefPtr<GstElementFactory> factory = adoptGRef(gst_element_factory_find("playbin" )); |
| 131 | return factory; |
| 132 | } |
| 133 | |
| 134 | MediaPlayerPrivateGStreamer::MediaPlayerPrivateGStreamer(MediaPlayer* player) |
| 135 | : MediaPlayerPrivateGStreamerBase(player) |
| 136 | , m_buffering(false) |
| 137 | , m_bufferingPercentage(0) |
| 138 | , m_cachedPosition(MediaTime::invalidTime()) |
| 139 | , m_cachedDuration(MediaTime::invalidTime()) |
| 140 | , m_canFallBackToLastFinishedSeekPosition(false) |
| 141 | , m_changingRate(false) |
| 142 | , m_downloadFinished(false) |
| 143 | , m_errorOccured(false) |
| 144 | , m_isEndReached(false) |
| 145 | , m_isStreaming(false) |
| 146 | , m_paused(true) |
| 147 | , m_playbackRate(1) |
| 148 | , m_requestedState(GST_STATE_VOID_PENDING) |
| 149 | , m_resetPipeline(false) |
| 150 | , m_seeking(false) |
| 151 | , m_seekIsPending(false) |
| 152 | , m_seekTime(MediaTime::invalidTime()) |
| 153 | , m_source(nullptr) |
| 154 | , m_volumeAndMuteInitialized(false) |
| 155 | , m_mediaLocations(nullptr) |
| 156 | , m_mediaLocationCurrentIndex(0) |
| 157 | , m_playbackRatePause(false) |
| 158 | , m_timeOfOverlappingSeek(MediaTime::invalidTime()) |
| 159 | , m_lastPlaybackRate(1) |
| 160 | , m_fillTimer(*this, &MediaPlayerPrivateGStreamer::fillTimerFired) |
| 161 | , m_maxTimeLoaded(MediaTime::zeroTime()) |
| 162 | , m_preload(player->preload()) |
| 163 | , m_delayingLoad(false) |
| 164 | , m_maxTimeLoadedAtLastDidLoadingProgress(MediaTime::zeroTime()) |
| 165 | , m_hasVideo(false) |
| 166 | , m_hasAudio(false) |
| 167 | , m_readyTimerHandler(RunLoop::main(), this, &MediaPlayerPrivateGStreamer::readyTimerFired) |
| 168 | , m_totalBytes(0) |
| 169 | , m_preservesPitch(false) |
| 170 | { |
| 171 | #if USE(GLIB) |
| 172 | m_readyTimerHandler.setPriority(G_PRIORITY_DEFAULT_IDLE); |
| 173 | #endif |
| 174 | } |
| 175 | |
| 176 | MediaPlayerPrivateGStreamer::~MediaPlayerPrivateGStreamer() |
| 177 | { |
| 178 | GST_DEBUG_OBJECT(pipeline(), "Disposing player" ); |
| 179 | |
| 180 | #if ENABLE(VIDEO_TRACK) |
| 181 | for (auto& track : m_audioTracks.values()) |
| 182 | track->disconnect(); |
| 183 | |
| 184 | for (auto& track : m_textTracks.values()) |
| 185 | track->disconnect(); |
| 186 | |
| 187 | for (auto& track : m_videoTracks.values()) |
| 188 | track->disconnect(); |
| 189 | #endif |
| 190 | if (m_fillTimer.isActive()) |
| 191 | m_fillTimer.stop(); |
| 192 | |
| 193 | if (m_mediaLocations) { |
| 194 | gst_structure_free(m_mediaLocations); |
| 195 | m_mediaLocations = nullptr; |
| 196 | } |
| 197 | |
| 198 | if (WEBKIT_IS_WEB_SRC(m_source.get()) && GST_OBJECT_PARENT(m_source.get())) |
| 199 | g_signal_handlers_disconnect_by_func(GST_ELEMENT_PARENT(m_source.get()), reinterpret_cast<gpointer>(uriDecodeBinElementAddedCallback), this); |
| 200 | |
| 201 | if (m_autoAudioSink) { |
| 202 | g_signal_handlers_disconnect_by_func(G_OBJECT(m_autoAudioSink.get()), |
| 203 | reinterpret_cast<gpointer>(setAudioStreamPropertiesCallback), this); |
| 204 | } |
| 205 | |
| 206 | m_readyTimerHandler.stop(); |
| 207 | for (auto& missingPluginCallback : m_missingPluginCallbacks) { |
| 208 | if (missingPluginCallback) |
| 209 | missingPluginCallback->invalidate(); |
| 210 | } |
| 211 | m_missingPluginCallbacks.clear(); |
| 212 | |
| 213 | if (m_videoSink) { |
| 214 | GRefPtr<GstPad> videoSinkPad = adoptGRef(gst_element_get_static_pad(m_videoSink.get(), "sink" )); |
| 215 | g_signal_handlers_disconnect_matched(videoSinkPad.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this); |
| 216 | } |
| 217 | |
| 218 | if (m_pipeline) { |
| 219 | GRefPtr<GstBus> bus = adoptGRef(gst_pipeline_get_bus(GST_PIPELINE(m_pipeline.get()))); |
| 220 | ASSERT(bus); |
| 221 | g_signal_handlers_disconnect_by_func(bus.get(), gpointer(busMessageCallback), this); |
| 222 | gst_bus_remove_signal_watch(bus.get()); |
| 223 | gst_bus_set_sync_handler(bus.get(), nullptr, nullptr, nullptr); |
| 224 | g_signal_handlers_disconnect_matched(m_pipeline.get(), G_SIGNAL_MATCH_DATA, 0, 0, nullptr, nullptr, this); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | static void convertToInternalProtocol(URL& url) |
| 229 | { |
| 230 | if (webkitGstCheckVersion(1, 12, 0)) |
| 231 | return; |
| 232 | if (url.protocolIsInHTTPFamily() || url.protocolIsBlob()) |
| 233 | url.setProtocol("webkit+" + url.protocol()); |
| 234 | } |
| 235 | |
| 236 | void MediaPlayerPrivateGStreamer::setPlaybinURL(const URL& url) |
| 237 | { |
| 238 | // Clean out everything after file:// url path. |
| 239 | String cleanURLString(url.string()); |
| 240 | if (url.isLocalFile()) |
| 241 | cleanURLString = cleanURLString.substring(0, url.pathEnd()); |
| 242 | |
| 243 | m_url = URL(URL(), cleanURLString); |
| 244 | convertToInternalProtocol(m_url); |
| 245 | GST_INFO_OBJECT(pipeline(), "Load %s" , m_url.string().utf8().data()); |
| 246 | g_object_set(m_pipeline.get(), "uri" , m_url.string().utf8().data(), nullptr); |
| 247 | } |
| 248 | |
| 249 | void MediaPlayerPrivateGStreamer::load(const String& urlString) |
| 250 | { |
| 251 | loadFull(urlString, String()); |
| 252 | } |
| 253 | |
| 254 | static void setSyncOnClock(GstElement *element, bool sync) |
| 255 | { |
| 256 | if (!GST_IS_BIN(element)) { |
| 257 | g_object_set(element, "sync" , sync, NULL); |
| 258 | return; |
| 259 | } |
| 260 | |
| 261 | GstIterator* it = gst_bin_iterate_sinks(GST_BIN(element)); |
| 262 | while (gst_iterator_foreach(it, (GstIteratorForeachFunction)([](const GValue* item, void* syncPtr) { |
| 263 | bool* sync = static_cast<bool*>(syncPtr); |
| 264 | setSyncOnClock(GST_ELEMENT(g_value_get_object(item)), *sync); |
| 265 | }), &sync) == GST_ITERATOR_RESYNC) |
| 266 | gst_iterator_resync(it); |
| 267 | gst_iterator_free(it); |
| 268 | } |
| 269 | |
| 270 | void MediaPlayerPrivateGStreamer::syncOnClock(bool sync) |
| 271 | { |
| 272 | setSyncOnClock(videoSink(), sync); |
| 273 | setSyncOnClock(audioSink(), sync); |
| 274 | } |
| 275 | |
| 276 | void MediaPlayerPrivateGStreamer::loadFull(const String& urlString, const String& pipelineName) |
| 277 | { |
| 278 | if (m_player->contentMIMEType() == "image/gif" ) { |
| 279 | loadingFailed(MediaPlayer::FormatError, MediaPlayer::HaveNothing, true); |
| 280 | return; |
| 281 | } |
| 282 | |
| 283 | URL url(URL(), urlString); |
| 284 | if (url.protocolIsAbout()) |
| 285 | return; |
| 286 | |
| 287 | if (!m_pipeline) |
| 288 | createGSTPlayBin(url, pipelineName); |
| 289 | syncOnClock(true); |
| 290 | if (m_fillTimer.isActive()) |
| 291 | m_fillTimer.stop(); |
| 292 | |
| 293 | ASSERT(m_pipeline); |
| 294 | |
| 295 | setPlaybinURL(url); |
| 296 | |
| 297 | GST_DEBUG_OBJECT(pipeline(), "preload: %s" , convertEnumerationToString(m_preload).utf8().data()); |
| 298 | if (m_preload == MediaPlayer::None) { |
| 299 | GST_INFO_OBJECT(pipeline(), "Delaying load." ); |
| 300 | m_delayingLoad = true; |
| 301 | } |
| 302 | |
| 303 | // Reset network and ready states. Those will be set properly once |
| 304 | // the pipeline pre-rolled. |
| 305 | m_networkState = MediaPlayer::Loading; |
| 306 | m_player->networkStateChanged(); |
| 307 | m_readyState = MediaPlayer::HaveNothing; |
| 308 | m_player->readyStateChanged(); |
| 309 | m_volumeAndMuteInitialized = false; |
| 310 | m_hasTaintedOrigin = WTF::nullopt; |
| 311 | |
| 312 | if (!m_delayingLoad) |
| 313 | commitLoad(); |
| 314 | } |
| 315 | |
| 316 | #if ENABLE(MEDIA_SOURCE) |
| 317 | void MediaPlayerPrivateGStreamer::load(const String&, MediaSourcePrivateClient*) |
| 318 | { |
| 319 | // Properly fail so the global MediaPlayer tries to fallback to the next MediaPlayerPrivate. |
| 320 | m_networkState = MediaPlayer::FormatError; |
| 321 | m_player->networkStateChanged(); |
| 322 | } |
| 323 | #endif |
| 324 | |
| 325 | #if ENABLE(MEDIA_STREAM) |
| 326 | void MediaPlayerPrivateGStreamer::load(MediaStreamPrivate& stream) |
| 327 | { |
| 328 | #if GST_CHECK_VERSION(1, 10, 0) |
| 329 | m_streamPrivate = &stream; |
| 330 | auto pipelineName = makeString("mediastream_" , |
| 331 | (stream.hasCaptureVideoSource() || stream.hasCaptureAudioSource()) ? "Local" : "Remote" , |
| 332 | "_0x" , hex(reinterpret_cast<uintptr_t>(this), Lowercase)); |
| 333 | |
| 334 | loadFull(String("mediastream://" ) + stream.id(), pipelineName); |
| 335 | syncOnClock(false); |
| 336 | |
| 337 | #if USE(GSTREAMER_GL) |
| 338 | ensureGLVideoSinkContext(); |
| 339 | #endif |
| 340 | m_player->play(); |
| 341 | #else |
| 342 | // Properly fail so the global MediaPlayer tries to fallback to the next MediaPlayerPrivate. |
| 343 | m_networkState = MediaPlayer::FormatError; |
| 344 | m_player->networkStateChanged(); |
| 345 | notImplemented(); |
| 346 | #endif |
| 347 | } |
| 348 | #endif |
| 349 | |
| 350 | void MediaPlayerPrivateGStreamer::commitLoad() |
| 351 | { |
| 352 | ASSERT(!m_delayingLoad); |
| 353 | GST_DEBUG_OBJECT(pipeline(), "Committing load." ); |
| 354 | |
| 355 | // GStreamer needs to have the pipeline set to a paused state to |
| 356 | // start providing anything useful. |
| 357 | changePipelineState(GST_STATE_PAUSED); |
| 358 | |
| 359 | setDownloadBuffering(); |
| 360 | updateStates(); |
| 361 | } |
| 362 | |
| 363 | MediaTime MediaPlayerPrivateGStreamer::playbackPosition() const |
| 364 | { |
| 365 | GST_TRACE_OBJECT(pipeline(), "isEndReached: %s, seeking: %s, seekTime: %s" , boolForPrinting(m_isEndReached), boolForPrinting(m_seeking), m_seekTime.toString().utf8().data()); |
| 366 | if (m_isEndReached && m_seeking) |
| 367 | return m_seekTime; |
| 368 | |
| 369 | // This constant should remain lower than HTMLMediaElement's maxTimeupdateEventFrequency. |
| 370 | static const Seconds positionCacheThreshold = 200_ms; |
| 371 | Seconds now = WTF::WallTime::now().secondsSinceEpoch(); |
| 372 | if (m_lastQueryTime && (now - m_lastQueryTime.value()) < positionCacheThreshold && m_cachedPosition.isValid()) { |
| 373 | GST_TRACE_OBJECT(pipeline(), "Returning cached position: %s" , m_cachedPosition.toString().utf8().data()); |
| 374 | return m_cachedPosition; |
| 375 | } |
| 376 | |
| 377 | m_lastQueryTime = now; |
| 378 | |
| 379 | // Position is only available if no async state change is going on and the state is either paused or playing. |
| 380 | gint64 position = GST_CLOCK_TIME_NONE; |
| 381 | GstQuery* query = gst_query_new_position(GST_FORMAT_TIME); |
| 382 | if (gst_element_query(m_pipeline.get(), query)) |
| 383 | gst_query_parse_position(query, 0, &position); |
| 384 | gst_query_unref(query); |
| 385 | |
| 386 | GST_TRACE_OBJECT(pipeline(), "Position %" GST_TIME_FORMAT ", canFallBackToLastFinishedSeekPosition: %s" , GST_TIME_ARGS(position), boolForPrinting(m_canFallBackToLastFinishedSeekPosition)); |
| 387 | |
| 388 | MediaTime playbackPosition = MediaTime::zeroTime(); |
| 389 | GstClockTime gstreamerPosition = static_cast<GstClockTime>(position); |
| 390 | if (GST_CLOCK_TIME_IS_VALID(gstreamerPosition)) |
| 391 | playbackPosition = MediaTime(gstreamerPosition, GST_SECOND); |
| 392 | else if (m_canFallBackToLastFinishedSeekPosition) |
| 393 | playbackPosition = m_seekTime; |
| 394 | |
| 395 | m_cachedPosition = playbackPosition; |
| 396 | return playbackPosition; |
| 397 | } |
| 398 | |
| 399 | void MediaPlayerPrivateGStreamer::readyTimerFired() |
| 400 | { |
| 401 | GST_DEBUG_OBJECT(pipeline(), "In READY for too long. Releasing pipeline resources." ); |
| 402 | changePipelineState(GST_STATE_NULL); |
| 403 | } |
| 404 | |
| 405 | bool MediaPlayerPrivateGStreamer::changePipelineState(GstState newState) |
| 406 | { |
| 407 | ASSERT(m_pipeline); |
| 408 | |
| 409 | GstState currentState; |
| 410 | GstState pending; |
| 411 | |
| 412 | gst_element_get_state(m_pipeline.get(), ¤tState, &pending, 0); |
| 413 | if (currentState == newState || pending == newState) { |
| 414 | GST_DEBUG_OBJECT(pipeline(), "Rejected state change to %s from %s with %s pending" , gst_element_state_get_name(newState), |
| 415 | gst_element_state_get_name(currentState), gst_element_state_get_name(pending)); |
| 416 | return true; |
| 417 | } |
| 418 | |
| 419 | GST_DEBUG_OBJECT(pipeline(), "Changing state change to %s from %s with %s pending" , gst_element_state_get_name(newState), |
| 420 | gst_element_state_get_name(currentState), gst_element_state_get_name(pending)); |
| 421 | |
| 422 | #if USE(GSTREAMER_GL) |
| 423 | if (currentState == GST_STATE_READY && newState == GST_STATE_PAUSED) |
| 424 | ensureGLVideoSinkContext(); |
| 425 | #endif |
| 426 | |
| 427 | GstStateChangeReturn setStateResult = gst_element_set_state(m_pipeline.get(), newState); |
| 428 | GstState pausedOrPlaying = newState == GST_STATE_PLAYING ? GST_STATE_PAUSED : GST_STATE_PLAYING; |
| 429 | if (currentState != pausedOrPlaying && setStateResult == GST_STATE_CHANGE_FAILURE) |
| 430 | return false; |
| 431 | |
| 432 | // Create a timer when entering the READY state so that we can free resources |
| 433 | // if we stay for too long on READY. |
| 434 | // Also lets remove the timer if we request a state change for any state other than READY. |
| 435 | // See also https://bugs.webkit.org/show_bug.cgi?id=117354 |
| 436 | if (newState == GST_STATE_READY && !m_readyTimerHandler.isActive()) { |
| 437 | // Max interval in seconds to stay in the READY state on manual |
| 438 | // state change requests. |
| 439 | static const Seconds readyStateTimerDelay { 1_min }; |
| 440 | m_readyTimerHandler.startOneShot(readyStateTimerDelay); |
| 441 | } else if (newState != GST_STATE_READY) |
| 442 | m_readyTimerHandler.stop(); |
| 443 | |
| 444 | return true; |
| 445 | } |
| 446 | |
| 447 | void MediaPlayerPrivateGStreamer::prepareToPlay() |
| 448 | { |
| 449 | GST_DEBUG_OBJECT(pipeline(), "Prepare to play" ); |
| 450 | m_preload = MediaPlayer::Auto; |
| 451 | if (m_delayingLoad) { |
| 452 | m_delayingLoad = false; |
| 453 | commitLoad(); |
| 454 | } |
| 455 | } |
| 456 | |
| 457 | void MediaPlayerPrivateGStreamer::play() |
| 458 | { |
| 459 | if (!m_playbackRate) { |
| 460 | m_playbackRatePause = true; |
| 461 | return; |
| 462 | } |
| 463 | |
| 464 | if (changePipelineState(GST_STATE_PLAYING)) { |
| 465 | m_isEndReached = false; |
| 466 | m_delayingLoad = false; |
| 467 | m_preload = MediaPlayer::Auto; |
| 468 | setDownloadBuffering(); |
| 469 | GST_INFO_OBJECT(pipeline(), "Play" ); |
| 470 | } else |
| 471 | loadingFailed(MediaPlayer::Empty); |
| 472 | } |
| 473 | |
| 474 | void MediaPlayerPrivateGStreamer::pause() |
| 475 | { |
| 476 | m_playbackRatePause = false; |
| 477 | GstState currentState, pendingState; |
| 478 | gst_element_get_state(m_pipeline.get(), ¤tState, &pendingState, 0); |
| 479 | if (currentState < GST_STATE_PAUSED && pendingState <= GST_STATE_PAUSED) |
| 480 | return; |
| 481 | |
| 482 | if (changePipelineState(GST_STATE_PAUSED)) |
| 483 | GST_INFO_OBJECT(pipeline(), "Pause" ); |
| 484 | else |
| 485 | loadingFailed(MediaPlayer::Empty); |
| 486 | } |
| 487 | |
| 488 | MediaTime MediaPlayerPrivateGStreamer::platformDuration() const |
| 489 | { |
| 490 | GST_TRACE_OBJECT(pipeline(), "errorOccured: %s, pipeline state: %s" , boolForPrinting(m_errorOccured), gst_element_state_get_name(GST_STATE(m_pipeline.get()))); |
| 491 | if (m_errorOccured) |
| 492 | return MediaTime::invalidTime(); |
| 493 | |
| 494 | // The duration query would fail on a not-prerolled pipeline. |
| 495 | if (GST_STATE(m_pipeline.get()) < GST_STATE_PAUSED) |
| 496 | return MediaTime::invalidTime(); |
| 497 | |
| 498 | int64_t duration = 0; |
| 499 | if (!gst_element_query_duration(m_pipeline.get(), GST_FORMAT_TIME, &duration) || !GST_CLOCK_TIME_IS_VALID(duration)) { |
| 500 | GST_DEBUG_OBJECT(pipeline(), "Time duration query failed for %s" , m_url.string().utf8().data()); |
| 501 | return MediaTime::positiveInfiniteTime(); |
| 502 | } |
| 503 | |
| 504 | GST_LOG_OBJECT(pipeline(), "Duration: %" GST_TIME_FORMAT, GST_TIME_ARGS(duration)); |
| 505 | return MediaTime(duration, GST_SECOND); |
| 506 | } |
| 507 | |
| 508 | MediaTime MediaPlayerPrivateGStreamer::durationMediaTime() const |
| 509 | { |
| 510 | GST_TRACE_OBJECT(pipeline(), "Cached duration: %s" , m_cachedDuration.toString().utf8().data()); |
| 511 | if (m_cachedDuration.isValid()) |
| 512 | return m_cachedDuration; |
| 513 | |
| 514 | MediaTime duration = platformDuration(); |
| 515 | if (!duration || duration.isInvalid()) |
| 516 | return MediaTime::zeroTime(); |
| 517 | |
| 518 | m_cachedDuration = duration; |
| 519 | |
| 520 | return m_cachedDuration; |
| 521 | } |
| 522 | |
| 523 | MediaTime MediaPlayerPrivateGStreamer::currentMediaTime() const |
| 524 | { |
| 525 | if (!m_pipeline || m_errorOccured) |
| 526 | return MediaTime::invalidTime(); |
| 527 | |
| 528 | GST_TRACE_OBJECT(pipeline(), "seeking: %s, seekTime: %s" , boolForPrinting(m_seeking), m_seekTime.toString().utf8().data()); |
| 529 | if (m_seeking) |
| 530 | return m_seekTime; |
| 531 | |
| 532 | return playbackPosition(); |
| 533 | } |
| 534 | |
| 535 | void MediaPlayerPrivateGStreamer::seek(const MediaTime& mediaTime) |
| 536 | { |
| 537 | if (!m_pipeline) |
| 538 | return; |
| 539 | |
| 540 | if (m_errorOccured) |
| 541 | return; |
| 542 | |
| 543 | GST_INFO_OBJECT(pipeline(), "[Seek] seek attempt to %s" , toString(mediaTime).utf8().data()); |
| 544 | |
| 545 | // Avoid useless seeking. |
| 546 | if (mediaTime == currentMediaTime()) { |
| 547 | GST_DEBUG_OBJECT(pipeline(), "[Seek] seek to EOS position unhandled" ); |
| 548 | return; |
| 549 | } |
| 550 | |
| 551 | MediaTime time = std::min(mediaTime, durationMediaTime()); |
| 552 | |
| 553 | if (isLiveStream()) { |
| 554 | GST_DEBUG_OBJECT(pipeline(), "[Seek] Live stream seek unhandled" ); |
| 555 | return; |
| 556 | } |
| 557 | |
| 558 | GST_INFO_OBJECT(pipeline(), "[Seek] seeking to %s" , toString(time).utf8().data()); |
| 559 | |
| 560 | if (m_seeking) { |
| 561 | m_timeOfOverlappingSeek = time; |
| 562 | if (m_seekIsPending) { |
| 563 | m_seekTime = time; |
| 564 | return; |
| 565 | } |
| 566 | } |
| 567 | |
| 568 | GstState state; |
| 569 | GstStateChangeReturn getStateResult = gst_element_get_state(m_pipeline.get(), &state, nullptr, 0); |
| 570 | if (getStateResult == GST_STATE_CHANGE_FAILURE || getStateResult == GST_STATE_CHANGE_NO_PREROLL) { |
| 571 | GST_DEBUG_OBJECT(pipeline(), "[Seek] cannot seek, current state change is %s" , gst_element_state_change_return_get_name(getStateResult)); |
| 572 | return; |
| 573 | } |
| 574 | if (getStateResult == GST_STATE_CHANGE_ASYNC || state < GST_STATE_PAUSED || m_isEndReached) { |
| 575 | m_seekIsPending = true; |
| 576 | if (m_isEndReached) { |
| 577 | GST_DEBUG_OBJECT(pipeline(), "[Seek] reset pipeline" ); |
| 578 | m_resetPipeline = true; |
| 579 | if (!changePipelineState(GST_STATE_PAUSED)) |
| 580 | loadingFailed(MediaPlayer::Empty); |
| 581 | } |
| 582 | } else { |
| 583 | // We can seek now. |
| 584 | if (!doSeek(time, m_player->rate(), static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE))) { |
| 585 | GST_DEBUG_OBJECT(pipeline(), "[Seek] seeking to %s failed" , toString(time).utf8().data()); |
| 586 | return; |
| 587 | } |
| 588 | } |
| 589 | |
| 590 | m_seeking = true; |
| 591 | m_seekTime = time; |
| 592 | m_isEndReached = false; |
| 593 | } |
| 594 | |
| 595 | bool MediaPlayerPrivateGStreamer::doSeek(const MediaTime& position, float rate, GstSeekFlags seekType) |
| 596 | { |
| 597 | // Default values for rate >= 0. |
| 598 | MediaTime startTime = position, endTime = MediaTime::invalidTime(); |
| 599 | |
| 600 | if (rate < 0) { |
| 601 | startTime = MediaTime::zeroTime(); |
| 602 | // If we are at beginning of media, start from the end to |
| 603 | // avoid immediate EOS. |
| 604 | if (position < MediaTime::zeroTime()) |
| 605 | endTime = durationMediaTime(); |
| 606 | else |
| 607 | endTime = position; |
| 608 | } |
| 609 | |
| 610 | if (!rate) |
| 611 | rate = 1.0; |
| 612 | |
| 613 | return gst_element_seek(m_pipeline.get(), rate, GST_FORMAT_TIME, seekType, |
| 614 | GST_SEEK_TYPE_SET, toGstClockTime(startTime), GST_SEEK_TYPE_SET, toGstClockTime(endTime)); |
| 615 | } |
| 616 | |
| 617 | void MediaPlayerPrivateGStreamer::updatePlaybackRate() |
| 618 | { |
| 619 | if (!m_changingRate) |
| 620 | return; |
| 621 | |
| 622 | GST_INFO_OBJECT(pipeline(), "Set Rate to %f" , m_playbackRate); |
| 623 | |
| 624 | // Mute the sound if the playback rate is negative or too extreme and audio pitch is not adjusted. |
| 625 | bool mute = m_playbackRate <= 0 || (!m_preservesPitch && (m_playbackRate < 0.8 || m_playbackRate > 2)); |
| 626 | |
| 627 | GST_INFO_OBJECT(pipeline(), mute ? "Need to mute audio" : "Do not need to mute audio" ); |
| 628 | |
| 629 | if (doSeek(playbackPosition(), m_playbackRate, static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH))) { |
| 630 | g_object_set(m_pipeline.get(), "mute" , mute, nullptr); |
| 631 | m_lastPlaybackRate = m_playbackRate; |
| 632 | } else { |
| 633 | m_playbackRate = m_lastPlaybackRate; |
| 634 | GST_ERROR("Set rate to %f failed" , m_playbackRate); |
| 635 | } |
| 636 | |
| 637 | if (m_playbackRatePause) { |
| 638 | GstState state; |
| 639 | GstState pending; |
| 640 | |
| 641 | gst_element_get_state(m_pipeline.get(), &state, &pending, 0); |
| 642 | if (state != GST_STATE_PLAYING && pending != GST_STATE_PLAYING) |
| 643 | changePipelineState(GST_STATE_PLAYING); |
| 644 | m_playbackRatePause = false; |
| 645 | } |
| 646 | |
| 647 | m_changingRate = false; |
| 648 | m_player->rateChanged(); |
| 649 | } |
| 650 | |
| 651 | bool MediaPlayerPrivateGStreamer::paused() const |
| 652 | { |
| 653 | if (m_isEndReached) { |
| 654 | GST_DEBUG_OBJECT(pipeline(), "Ignoring pause at EOS" ); |
| 655 | return true; |
| 656 | } |
| 657 | |
| 658 | if (m_playbackRatePause) { |
| 659 | GST_DEBUG_OBJECT(pipeline(), "Playback rate is 0, simulating PAUSED state" ); |
| 660 | return false; |
| 661 | } |
| 662 | |
| 663 | GstState state; |
| 664 | gst_element_get_state(m_pipeline.get(), &state, nullptr, 0); |
| 665 | bool paused = state <= GST_STATE_PAUSED; |
| 666 | GST_DEBUG_OBJECT(pipeline(), "Paused: %s" , toString(paused).utf8().data()); |
| 667 | return paused; |
| 668 | } |
| 669 | |
| 670 | bool MediaPlayerPrivateGStreamer::seeking() const |
| 671 | { |
| 672 | return m_seeking; |
| 673 | } |
| 674 | |
| 675 | #if GST_CHECK_VERSION(1, 10, 0) |
| 676 | #define CLEAR_TRACKS(tracks, method) \ |
| 677 | for (auto& track : tracks.values())\ |
| 678 | method(*track);\ |
| 679 | tracks.clear(); |
| 680 | |
| 681 | void MediaPlayerPrivateGStreamer::clearTracks() |
| 682 | { |
| 683 | #if ENABLE(VIDEO_TRACK) |
| 684 | CLEAR_TRACKS(m_audioTracks, m_player->removeAudioTrack); |
| 685 | CLEAR_TRACKS(m_videoTracks, m_player->removeVideoTrack); |
| 686 | CLEAR_TRACKS(m_textTracks, m_player->removeTextTrack); |
| 687 | #endif // ENABLE(VIDEO_TRACK) |
| 688 | } |
| 689 | #undef CLEAR_TRACKS |
| 690 | |
| 691 | #if ENABLE(VIDEO_TRACK) |
| 692 | #define CREATE_TRACK(type, Type) \ |
| 693 | m_has##Type = true; \ |
| 694 | if (!useMediaSource) {\ |
| 695 | RefPtr<Type##TrackPrivateGStreamer> track = Type##TrackPrivateGStreamer::create(makeWeakPtr(*this), i, stream); \ |
| 696 | m_##type##Tracks.add(track->id(), track); \ |
| 697 | m_player->add##Type##Track(*track);\ |
| 698 | if (gst_stream_get_stream_flags(stream.get()) & GST_STREAM_FLAG_SELECT) { \ |
| 699 | m_current##Type##StreamId = String(gst_stream_get_stream_id(stream.get())); \ |
| 700 | } \ |
| 701 | } |
| 702 | |
| 703 | FloatSize MediaPlayerPrivateGStreamer::naturalSize() const |
| 704 | { |
| 705 | #if ENABLE(MEDIA_STREAM) |
| 706 | if (!m_isLegacyPlaybin && !m_currentVideoStreamId.isEmpty()) { |
| 707 | RefPtr<VideoTrackPrivateGStreamer> videoTrack = m_videoTracks.get(m_currentVideoStreamId); |
| 708 | |
| 709 | if (videoTrack) { |
| 710 | auto tags = adoptGRef(gst_stream_get_tags(videoTrack->stream())); |
| 711 | gint width, height; |
| 712 | |
| 713 | if (tags && gst_tag_list_get_int(tags.get(), WEBKIT_MEDIA_TRACK_TAG_WIDTH, &width) && gst_tag_list_get_int(tags.get(), WEBKIT_MEDIA_TRACK_TAG_HEIGHT, &height)) |
| 714 | return FloatSize(width, height); |
| 715 | } |
| 716 | } |
| 717 | #endif // ENABLE(MEDIA_STREAM) |
| 718 | |
| 719 | return MediaPlayerPrivateGStreamerBase::naturalSize(); |
| 720 | } |
| 721 | #else |
| 722 | #define CREATE_TRACK(type, _id, tracks, method, stream) m_has##Type## = true; |
| 723 | #endif // ENABLE(VIDEO_TRACK) |
| 724 | |
| 725 | void MediaPlayerPrivateGStreamer::updateTracks() |
| 726 | { |
| 727 | ASSERT(!m_isLegacyPlaybin); |
| 728 | |
| 729 | bool useMediaSource = isMediaSource(); |
| 730 | unsigned length = gst_stream_collection_get_size(m_streamCollection.get()); |
| 731 | |
| 732 | bool oldHasAudio = m_hasAudio; |
| 733 | bool oldHasVideo = m_hasVideo; |
| 734 | // New stream collections override previous ones. |
| 735 | clearTracks(); |
| 736 | unsigned textTrackIndex = 0; |
| 737 | for (unsigned i = 0; i < length; i++) { |
| 738 | GRefPtr<GstStream> stream = gst_stream_collection_get_stream(m_streamCollection.get(), i); |
| 739 | String streamId(gst_stream_get_stream_id(stream.get())); |
| 740 | GstStreamType type = gst_stream_get_stream_type(stream.get()); |
| 741 | |
| 742 | GST_DEBUG_OBJECT(pipeline(), "Inspecting %s track with ID %s" , gst_stream_type_get_name(type), streamId.utf8().data()); |
| 743 | if (type & GST_STREAM_TYPE_AUDIO) { |
| 744 | CREATE_TRACK(audio, Audio) |
| 745 | } else if (type & GST_STREAM_TYPE_VIDEO) { |
| 746 | CREATE_TRACK(video, Video) |
| 747 | } else if (type & GST_STREAM_TYPE_TEXT && !useMediaSource) { |
| 748 | #if ENABLE(VIDEO_TRACK) |
| 749 | auto track = InbandTextTrackPrivateGStreamer::create(textTrackIndex++, stream); |
| 750 | m_textTracks.add(streamId, track.copyRef()); |
| 751 | m_player->addTextTrack(track.get()); |
| 752 | #endif |
| 753 | } else |
| 754 | GST_WARNING("Unknown track type found for stream %s" , streamId.utf8().data()); |
| 755 | } |
| 756 | |
| 757 | if ((oldHasVideo != m_hasVideo) || (oldHasAudio != m_hasAudio)) |
| 758 | m_player->characteristicChanged(); |
| 759 | |
| 760 | if (m_hasVideo) |
| 761 | m_player->sizeChanged(); |
| 762 | |
| 763 | m_player->client().mediaPlayerEngineUpdated(m_player); |
| 764 | } |
| 765 | #endif // GST_CHECK_VERSION(1, 10, 0) |
| 766 | |
| 767 | void MediaPlayerPrivateGStreamer::enableTrack(TrackPrivateBaseGStreamer::TrackType trackType, unsigned index) |
| 768 | { |
| 769 | // FIXME: Remove isMediaSource() test below when fixing https://bugs.webkit.org/show_bug.cgi?id=182531. |
| 770 | if (isMediaSource()) { |
| 771 | GST_FIXME_OBJECT(m_pipeline.get(), "Audio/Video/Text track switching is not yet supported by the MSE backend." ); |
| 772 | return; |
| 773 | } |
| 774 | |
| 775 | const char* propertyName; |
| 776 | const char* trackTypeAsString; |
| 777 | Vector<String> selectedStreams; |
| 778 | String selectedStreamId; |
| 779 | |
| 780 | #if GST_CHECK_VERSION(1, 10, 0) |
| 781 | GstStream* stream = nullptr; |
| 782 | |
| 783 | if (!m_isLegacyPlaybin) { |
| 784 | stream = gst_stream_collection_get_stream(m_streamCollection.get(), index); |
| 785 | if (!stream) { |
| 786 | GST_WARNING_OBJECT(pipeline(), "No stream to select at index %u" , index); |
| 787 | return; |
| 788 | } |
| 789 | selectedStreamId = String::fromUTF8(gst_stream_get_stream_id(stream)); |
| 790 | selectedStreams.append(selectedStreamId); |
| 791 | } |
| 792 | #endif // GST_CHECK_VERSION(1,0,0) |
| 793 | |
| 794 | switch (trackType) { |
| 795 | case TrackPrivateBaseGStreamer::TrackType::Audio: |
| 796 | propertyName = "current-audio" ; |
| 797 | trackTypeAsString = "audio" ; |
| 798 | if (!selectedStreamId.isEmpty() && selectedStreamId == m_currentAudioStreamId) { |
| 799 | GST_INFO_OBJECT(pipeline(), "%s stream: %s already selected, not doing anything." , trackTypeAsString, selectedStreamId.utf8().data()); |
| 800 | return; |
| 801 | } |
| 802 | |
| 803 | if (!m_currentTextStreamId.isEmpty()) |
| 804 | selectedStreams.append(m_currentTextStreamId); |
| 805 | if (!m_currentVideoStreamId.isEmpty()) |
| 806 | selectedStreams.append(m_currentVideoStreamId); |
| 807 | break; |
| 808 | case TrackPrivateBaseGStreamer::TrackType::Video: |
| 809 | propertyName = "current-video" ; |
| 810 | trackTypeAsString = "video" ; |
| 811 | if (!selectedStreamId.isEmpty() && selectedStreamId == m_currentVideoStreamId) { |
| 812 | GST_INFO_OBJECT(pipeline(), "%s stream: %s already selected, not doing anything." , trackTypeAsString, selectedStreamId.utf8().data()); |
| 813 | return; |
| 814 | } |
| 815 | |
| 816 | if (!m_currentAudioStreamId.isEmpty()) |
| 817 | selectedStreams.append(m_currentAudioStreamId); |
| 818 | if (!m_currentTextStreamId.isEmpty()) |
| 819 | selectedStreams.append(m_currentTextStreamId); |
| 820 | break; |
| 821 | case TrackPrivateBaseGStreamer::TrackType::Text: |
| 822 | propertyName = "current-text" ; |
| 823 | trackTypeAsString = "text" ; |
| 824 | if (!selectedStreamId.isEmpty() && selectedStreamId == m_currentTextStreamId) { |
| 825 | GST_INFO_OBJECT(pipeline(), "%s stream: %s already selected, not doing anything." , trackTypeAsString, selectedStreamId.utf8().data()); |
| 826 | return; |
| 827 | } |
| 828 | |
| 829 | if (!m_currentAudioStreamId.isEmpty()) |
| 830 | selectedStreams.append(m_currentAudioStreamId); |
| 831 | if (!m_currentVideoStreamId.isEmpty()) |
| 832 | selectedStreams.append(m_currentVideoStreamId); |
| 833 | break; |
| 834 | case TrackPrivateBaseGStreamer::TrackType::Unknown: |
| 835 | default: |
| 836 | ASSERT_NOT_REACHED(); |
| 837 | } |
| 838 | |
| 839 | GST_INFO_OBJECT(pipeline(), "Enabling %s track with index: %u" , trackTypeAsString, index); |
| 840 | if (m_isLegacyPlaybin) |
| 841 | g_object_set(m_pipeline.get(), propertyName, index, nullptr); |
| 842 | #if GST_CHECK_VERSION(1, 10, 0) |
| 843 | else { |
| 844 | GList* selectedStreamsList = nullptr; |
| 845 | |
| 846 | for (const auto& streamId : selectedStreams) |
| 847 | selectedStreamsList = g_list_append(selectedStreamsList, g_strdup(streamId.utf8().data())); |
| 848 | |
| 849 | // TODO: MSE GstStream API support: https://bugs.webkit.org/show_bug.cgi?id=182531 |
| 850 | gst_element_send_event(m_pipeline.get(), gst_event_new_select_streams(selectedStreamsList)); |
| 851 | g_list_free_full(selectedStreamsList, reinterpret_cast<GDestroyNotify>(g_free)); |
| 852 | } |
| 853 | #endif |
| 854 | } |
| 855 | |
| 856 | void MediaPlayerPrivateGStreamer::videoChangedCallback(MediaPlayerPrivateGStreamer* player) |
| 857 | { |
| 858 | player->m_notifier->notify(MainThreadNotification::VideoChanged, [player] { |
| 859 | player->notifyPlayerOfVideo(); |
| 860 | }); |
| 861 | } |
| 862 | |
| 863 | void MediaPlayerPrivateGStreamer::notifyPlayerOfVideo() |
| 864 | { |
| 865 | if (UNLIKELY(!m_pipeline || !m_source)) |
| 866 | return; |
| 867 | |
| 868 | ASSERT(m_isLegacyPlaybin || isMediaSource()); |
| 869 | |
| 870 | gint numTracks = 0; |
| 871 | bool useMediaSource = isMediaSource(); |
| 872 | GstElement* element = useMediaSource ? m_source.get() : m_pipeline.get(); |
| 873 | g_object_get(element, "n-video" , &numTracks, nullptr); |
| 874 | |
| 875 | GST_INFO_OBJECT(pipeline(), "Media has %d video tracks" , numTracks); |
| 876 | |
| 877 | bool oldHasVideo = m_hasVideo; |
| 878 | m_hasVideo = numTracks > 0; |
| 879 | if (oldHasVideo != m_hasVideo) |
| 880 | m_player->characteristicChanged(); |
| 881 | |
| 882 | if (m_hasVideo) |
| 883 | m_player->sizeChanged(); |
| 884 | |
| 885 | if (useMediaSource) { |
| 886 | GST_DEBUG_OBJECT(pipeline(), "Tracks managed by source element. Bailing out now." ); |
| 887 | m_player->client().mediaPlayerEngineUpdated(m_player); |
| 888 | return; |
| 889 | } |
| 890 | |
| 891 | #if ENABLE(VIDEO_TRACK) |
| 892 | Vector<String> validVideoStreams; |
| 893 | for (gint i = 0; i < numTracks; ++i) { |
| 894 | GRefPtr<GstPad> pad; |
| 895 | g_signal_emit_by_name(m_pipeline.get(), "get-video-pad" , i, &pad.outPtr(), nullptr); |
| 896 | ASSERT(pad); |
| 897 | |
| 898 | String streamId = "V" + String::number(i); |
| 899 | validVideoStreams.append(streamId); |
| 900 | if (i < static_cast<gint>(m_videoTracks.size())) { |
| 901 | RefPtr<VideoTrackPrivateGStreamer> existingTrack = m_videoTracks.get(streamId); |
| 902 | if (existingTrack) { |
| 903 | existingTrack->setIndex(i); |
| 904 | if (existingTrack->pad() == pad) |
| 905 | continue; |
| 906 | } |
| 907 | } |
| 908 | |
| 909 | auto track = VideoTrackPrivateGStreamer::create(makeWeakPtr(*this), i, pad); |
| 910 | ASSERT(streamId == track->id()); |
| 911 | m_videoTracks.add(streamId, track.copyRef()); |
| 912 | m_player->addVideoTrack(track.get()); |
| 913 | } |
| 914 | |
| 915 | purgeInvalidVideoTracks(validVideoStreams); |
| 916 | #endif |
| 917 | |
| 918 | m_player->client().mediaPlayerEngineUpdated(m_player); |
| 919 | } |
| 920 | |
| 921 | void MediaPlayerPrivateGStreamer::videoSinkCapsChangedCallback(MediaPlayerPrivateGStreamer* player) |
| 922 | { |
| 923 | player->m_notifier->notify(MainThreadNotification::VideoCapsChanged, [player] { |
| 924 | player->notifyPlayerOfVideoCaps(); |
| 925 | }); |
| 926 | } |
| 927 | |
| 928 | void MediaPlayerPrivateGStreamer::notifyPlayerOfVideoCaps() |
| 929 | { |
| 930 | m_videoSize = IntSize(); |
| 931 | m_player->client().mediaPlayerEngineUpdated(m_player); |
| 932 | } |
| 933 | |
| 934 | void MediaPlayerPrivateGStreamer::audioChangedCallback(MediaPlayerPrivateGStreamer* player) |
| 935 | { |
| 936 | player->m_notifier->notify(MainThreadNotification::AudioChanged, [player] { |
| 937 | player->notifyPlayerOfAudio(); |
| 938 | }); |
| 939 | } |
| 940 | |
| 941 | void MediaPlayerPrivateGStreamer::notifyPlayerOfAudio() |
| 942 | { |
| 943 | if (UNLIKELY(!m_pipeline || !m_source)) |
| 944 | return; |
| 945 | |
| 946 | ASSERT(m_isLegacyPlaybin || isMediaSource()); |
| 947 | |
| 948 | gint numTracks = 0; |
| 949 | bool useMediaSource = isMediaSource(); |
| 950 | GstElement* element = useMediaSource ? m_source.get() : m_pipeline.get(); |
| 951 | g_object_get(element, "n-audio" , &numTracks, nullptr); |
| 952 | |
| 953 | GST_INFO_OBJECT(pipeline(), "Media has %d audio tracks" , numTracks); |
| 954 | bool oldHasAudio = m_hasAudio; |
| 955 | m_hasAudio = numTracks > 0; |
| 956 | if (oldHasAudio != m_hasAudio) |
| 957 | m_player->characteristicChanged(); |
| 958 | |
| 959 | if (useMediaSource) { |
| 960 | GST_DEBUG_OBJECT(pipeline(), "Tracks managed by source element. Bailing out now." ); |
| 961 | m_player->client().mediaPlayerEngineUpdated(m_player); |
| 962 | return; |
| 963 | } |
| 964 | |
| 965 | #if ENABLE(VIDEO_TRACK) |
| 966 | Vector<String> validAudioStreams; |
| 967 | for (gint i = 0; i < numTracks; ++i) { |
| 968 | GRefPtr<GstPad> pad; |
| 969 | g_signal_emit_by_name(m_pipeline.get(), "get-audio-pad" , i, &pad.outPtr(), nullptr); |
| 970 | ASSERT(pad); |
| 971 | |
| 972 | String streamId = "A" + String::number(i); |
| 973 | validAudioStreams.append(streamId); |
| 974 | if (i < static_cast<gint>(m_audioTracks.size())) { |
| 975 | RefPtr<AudioTrackPrivateGStreamer> existingTrack = m_audioTracks.get(streamId); |
| 976 | if (existingTrack) { |
| 977 | existingTrack->setIndex(i); |
| 978 | if (existingTrack->pad() == pad) |
| 979 | continue; |
| 980 | } |
| 981 | } |
| 982 | |
| 983 | auto track = AudioTrackPrivateGStreamer::create(makeWeakPtr(*this), i, pad); |
| 984 | ASSERT(streamId == track->id()); |
| 985 | m_audioTracks.add(streamId, track); |
| 986 | m_player->addAudioTrack(*track); |
| 987 | } |
| 988 | |
| 989 | purgeInvalidAudioTracks(validAudioStreams); |
| 990 | #endif |
| 991 | |
| 992 | m_player->client().mediaPlayerEngineUpdated(m_player); |
| 993 | } |
| 994 | |
| 995 | #if ENABLE(VIDEO_TRACK) |
| 996 | void MediaPlayerPrivateGStreamer::textChangedCallback(MediaPlayerPrivateGStreamer* player) |
| 997 | { |
| 998 | player->m_notifier->notify(MainThreadNotification::TextChanged, [player] { |
| 999 | player->notifyPlayerOfText(); |
| 1000 | }); |
| 1001 | } |
| 1002 | |
| 1003 | void MediaPlayerPrivateGStreamer::notifyPlayerOfText() |
| 1004 | { |
| 1005 | if (UNLIKELY(!m_pipeline || !m_source)) |
| 1006 | return; |
| 1007 | |
| 1008 | ASSERT(m_isLegacyPlaybin || isMediaSource()); |
| 1009 | |
| 1010 | gint numTracks = 0; |
| 1011 | bool useMediaSource = isMediaSource(); |
| 1012 | GstElement* element = useMediaSource ? m_source.get() : m_pipeline.get(); |
| 1013 | g_object_get(element, "n-text" , &numTracks, nullptr); |
| 1014 | |
| 1015 | GST_INFO_OBJECT(pipeline(), "Media has %d text tracks" , numTracks); |
| 1016 | |
| 1017 | if (useMediaSource) { |
| 1018 | GST_DEBUG_OBJECT(pipeline(), "Tracks managed by source element. Bailing out now." ); |
| 1019 | return; |
| 1020 | } |
| 1021 | |
| 1022 | Vector<String> validTextStreams; |
| 1023 | for (gint i = 0; i < numTracks; ++i) { |
| 1024 | GRefPtr<GstPad> pad; |
| 1025 | g_signal_emit_by_name(m_pipeline.get(), "get-text-pad" , i, &pad.outPtr(), nullptr); |
| 1026 | ASSERT(pad); |
| 1027 | |
| 1028 | // We can't assume the pad has a sticky event here like implemented in |
| 1029 | // InbandTextTrackPrivateGStreamer because it might be emitted after the |
| 1030 | // track was created. So fallback to a dummy stream ID like in the Audio |
| 1031 | // and Video tracks. |
| 1032 | String streamId = "T" + String::number(i); |
| 1033 | |
| 1034 | validTextStreams.append(streamId); |
| 1035 | if (i < static_cast<gint>(m_textTracks.size())) { |
| 1036 | RefPtr<InbandTextTrackPrivateGStreamer> existingTrack = m_textTracks.get(streamId); |
| 1037 | if (existingTrack) { |
| 1038 | existingTrack->setIndex(i); |
| 1039 | if (existingTrack->pad() == pad) |
| 1040 | continue; |
| 1041 | } |
| 1042 | } |
| 1043 | |
| 1044 | auto track = InbandTextTrackPrivateGStreamer::create(i, pad); |
| 1045 | m_textTracks.add(streamId, track.copyRef()); |
| 1046 | m_player->addTextTrack(track.get()); |
| 1047 | } |
| 1048 | |
| 1049 | purgeInvalidTextTracks(validTextStreams); |
| 1050 | } |
| 1051 | |
| 1052 | GstFlowReturn MediaPlayerPrivateGStreamer::newTextSampleCallback(MediaPlayerPrivateGStreamer* player) |
| 1053 | { |
| 1054 | player->newTextSample(); |
| 1055 | return GST_FLOW_OK; |
| 1056 | } |
| 1057 | |
| 1058 | void MediaPlayerPrivateGStreamer::newTextSample() |
| 1059 | { |
| 1060 | if (!m_textAppSink) |
| 1061 | return; |
| 1062 | |
| 1063 | GRefPtr<GstEvent> streamStartEvent = adoptGRef( |
| 1064 | gst_pad_get_sticky_event(m_textAppSinkPad.get(), GST_EVENT_STREAM_START, 0)); |
| 1065 | |
| 1066 | GRefPtr<GstSample> sample; |
| 1067 | g_signal_emit_by_name(m_textAppSink.get(), "pull-sample" , &sample.outPtr(), nullptr); |
| 1068 | ASSERT(sample); |
| 1069 | |
| 1070 | if (streamStartEvent) { |
| 1071 | bool found = FALSE; |
| 1072 | const gchar* id; |
| 1073 | gst_event_parse_stream_start(streamStartEvent.get(), &id); |
| 1074 | for (auto& track : m_textTracks.values()) { |
| 1075 | if (!strcmp(track->streamId().utf8().data(), id)) { |
| 1076 | track->handleSample(sample); |
| 1077 | found = true; |
| 1078 | break; |
| 1079 | } |
| 1080 | } |
| 1081 | if (!found) |
| 1082 | GST_WARNING("Got sample with unknown stream ID %s." , id); |
| 1083 | } else |
| 1084 | GST_WARNING("Unable to handle sample with no stream start event." ); |
| 1085 | } |
| 1086 | #endif |
| 1087 | |
| 1088 | void MediaPlayerPrivateGStreamer::setRate(float rate) |
| 1089 | { |
| 1090 | // Higher rate causes crash. |
| 1091 | rate = clampTo(rate, -20.0, 20.0); |
| 1092 | |
| 1093 | // Avoid useless playback rate update. |
| 1094 | if (m_playbackRate == rate) { |
| 1095 | // and make sure that upper layers were notified if rate was set |
| 1096 | |
| 1097 | if (!m_changingRate && m_player->rate() != m_playbackRate) |
| 1098 | m_player->rateChanged(); |
| 1099 | return; |
| 1100 | } |
| 1101 | |
| 1102 | if (isLiveStream()) { |
| 1103 | // notify upper layers that we cannot handle passed rate. |
| 1104 | m_changingRate = false; |
| 1105 | m_player->rateChanged(); |
| 1106 | return; |
| 1107 | } |
| 1108 | |
| 1109 | GstState state; |
| 1110 | GstState pending; |
| 1111 | |
| 1112 | m_playbackRate = rate; |
| 1113 | m_changingRate = true; |
| 1114 | |
| 1115 | gst_element_get_state(m_pipeline.get(), &state, &pending, 0); |
| 1116 | |
| 1117 | if (!rate) { |
| 1118 | m_changingRate = false; |
| 1119 | m_playbackRatePause = true; |
| 1120 | if (state != GST_STATE_PAUSED && pending != GST_STATE_PAUSED) |
| 1121 | changePipelineState(GST_STATE_PAUSED); |
| 1122 | return; |
| 1123 | } |
| 1124 | |
| 1125 | if ((state != GST_STATE_PLAYING && state != GST_STATE_PAUSED) |
| 1126 | || (pending == GST_STATE_PAUSED)) |
| 1127 | return; |
| 1128 | |
| 1129 | updatePlaybackRate(); |
| 1130 | } |
| 1131 | |
| 1132 | double MediaPlayerPrivateGStreamer::rate() const |
| 1133 | { |
| 1134 | return m_playbackRate; |
| 1135 | } |
| 1136 | |
| 1137 | void MediaPlayerPrivateGStreamer::setPreservesPitch(bool preservesPitch) |
| 1138 | { |
| 1139 | m_preservesPitch = preservesPitch; |
| 1140 | } |
| 1141 | |
| 1142 | std::unique_ptr<PlatformTimeRanges> MediaPlayerPrivateGStreamer::buffered() const |
| 1143 | { |
| 1144 | auto timeRanges = std::make_unique<PlatformTimeRanges>(); |
| 1145 | if (m_errorOccured || isLiveStream()) |
| 1146 | return timeRanges; |
| 1147 | |
| 1148 | MediaTime mediaDuration = durationMediaTime(); |
| 1149 | if (!mediaDuration || mediaDuration.isPositiveInfinite()) |
| 1150 | return timeRanges; |
| 1151 | |
| 1152 | GstQuery* query = gst_query_new_buffering(GST_FORMAT_PERCENT); |
| 1153 | |
| 1154 | if (!gst_element_query(m_pipeline.get(), query)) { |
| 1155 | gst_query_unref(query); |
| 1156 | return timeRanges; |
| 1157 | } |
| 1158 | |
| 1159 | guint numBufferingRanges = gst_query_get_n_buffering_ranges(query); |
| 1160 | for (guint index = 0; index < numBufferingRanges; index++) { |
| 1161 | gint64 rangeStart = 0, rangeStop = 0; |
| 1162 | if (gst_query_parse_nth_buffering_range(query, index, &rangeStart, &rangeStop)) { |
| 1163 | uint64_t startTime = gst_util_uint64_scale_int_round(toGstUnsigned64Time(mediaDuration), rangeStart, GST_FORMAT_PERCENT_MAX); |
| 1164 | uint64_t stopTime = gst_util_uint64_scale_int_round(toGstUnsigned64Time(mediaDuration), rangeStop, GST_FORMAT_PERCENT_MAX); |
| 1165 | timeRanges->add(MediaTime(startTime, GST_SECOND), MediaTime(stopTime, GST_SECOND)); |
| 1166 | } |
| 1167 | } |
| 1168 | |
| 1169 | // Fallback to the more general maxTimeLoaded() if no range has |
| 1170 | // been found. |
| 1171 | if (!timeRanges->length()) { |
| 1172 | MediaTime loaded = maxTimeLoaded(); |
| 1173 | if (loaded.isValid() && loaded) |
| 1174 | timeRanges->add(MediaTime::zeroTime(), loaded); |
| 1175 | } |
| 1176 | |
| 1177 | gst_query_unref(query); |
| 1178 | |
| 1179 | return timeRanges; |
| 1180 | } |
| 1181 | |
| 1182 | void MediaPlayerPrivateGStreamer::handleMessage(GstMessage* message) |
| 1183 | { |
| 1184 | GUniqueOutPtr<GError> err; |
| 1185 | GUniqueOutPtr<gchar> debug; |
| 1186 | MediaPlayer::NetworkState error; |
| 1187 | bool issueError = true; |
| 1188 | bool attemptNextLocation = false; |
| 1189 | const GstStructure* structure = gst_message_get_structure(message); |
| 1190 | GstState requestedState, currentState; |
| 1191 | |
| 1192 | m_canFallBackToLastFinishedSeekPosition = false; |
| 1193 | |
| 1194 | if (structure) { |
| 1195 | const gchar* messageTypeName = gst_structure_get_name(structure); |
| 1196 | |
| 1197 | // Redirect messages are sent from elements, like qtdemux, to |
| 1198 | // notify of the new location(s) of the media. |
| 1199 | if (!g_strcmp0(messageTypeName, "redirect" )) { |
| 1200 | mediaLocationChanged(message); |
| 1201 | return; |
| 1202 | } |
| 1203 | } |
| 1204 | |
| 1205 | // We ignore state changes from internal elements. They are forwarded to playbin2 anyway. |
| 1206 | bool messageSourceIsPlaybin = GST_MESSAGE_SRC(message) == reinterpret_cast<GstObject*>(m_pipeline.get()); |
| 1207 | |
| 1208 | GST_LOG_OBJECT(pipeline(), "Message %s received from element %s" , GST_MESSAGE_TYPE_NAME(message), GST_MESSAGE_SRC_NAME(message)); |
| 1209 | switch (GST_MESSAGE_TYPE(message)) { |
| 1210 | case GST_MESSAGE_ERROR: |
| 1211 | if (m_resetPipeline || !m_missingPluginCallbacks.isEmpty() || m_errorOccured) |
| 1212 | break; |
| 1213 | gst_message_parse_error(message, &err.outPtr(), &debug.outPtr()); |
| 1214 | GST_ERROR("Error %d: %s (url=%s)" , err->code, err->message, m_url.string().utf8().data()); |
| 1215 | |
| 1216 | GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(m_pipeline.get()), GST_DEBUG_GRAPH_SHOW_ALL, "webkit-video.error" ); |
| 1217 | |
| 1218 | error = MediaPlayer::Empty; |
| 1219 | if (g_error_matches(err.get(), GST_STREAM_ERROR, GST_STREAM_ERROR_CODEC_NOT_FOUND) |
| 1220 | || g_error_matches(err.get(), GST_STREAM_ERROR, GST_STREAM_ERROR_WRONG_TYPE) |
| 1221 | || g_error_matches(err.get(), GST_STREAM_ERROR, GST_STREAM_ERROR_FAILED) |
| 1222 | || g_error_matches(err.get(), GST_CORE_ERROR, GST_CORE_ERROR_MISSING_PLUGIN) |
| 1223 | || g_error_matches(err.get(), GST_RESOURCE_ERROR, GST_RESOURCE_ERROR_NOT_FOUND)) |
| 1224 | error = MediaPlayer::FormatError; |
| 1225 | else if (g_error_matches(err.get(), GST_STREAM_ERROR, GST_STREAM_ERROR_TYPE_NOT_FOUND)) { |
| 1226 | // Let the mediaPlayerClient handle the stream error, in |
| 1227 | // this case the HTMLMediaElement will emit a stalled |
| 1228 | // event. |
| 1229 | GST_ERROR("Decode error, let the Media element emit a stalled event." ); |
| 1230 | m_loadingStalled = true; |
| 1231 | break; |
| 1232 | } else if (err->domain == GST_STREAM_ERROR) { |
| 1233 | error = MediaPlayer::DecodeError; |
| 1234 | attemptNextLocation = true; |
| 1235 | } else if (err->domain == GST_RESOURCE_ERROR) |
| 1236 | error = MediaPlayer::NetworkError; |
| 1237 | |
| 1238 | if (attemptNextLocation) |
| 1239 | issueError = !loadNextLocation(); |
| 1240 | if (issueError) { |
| 1241 | m_errorOccured = true; |
| 1242 | if (m_networkState != error) { |
| 1243 | m_networkState = error; |
| 1244 | m_player->networkStateChanged(); |
| 1245 | } |
| 1246 | } |
| 1247 | break; |
| 1248 | case GST_MESSAGE_EOS: |
| 1249 | didEnd(); |
| 1250 | break; |
| 1251 | case GST_MESSAGE_ASYNC_DONE: |
| 1252 | if (!messageSourceIsPlaybin || m_delayingLoad) |
| 1253 | break; |
| 1254 | asyncStateChangeDone(); |
| 1255 | break; |
| 1256 | case GST_MESSAGE_STATE_CHANGED: { |
| 1257 | if (!messageSourceIsPlaybin || m_delayingLoad) |
| 1258 | break; |
| 1259 | updateStates(); |
| 1260 | |
| 1261 | // Construct a filename for the graphviz dot file output. |
| 1262 | GstState newState; |
| 1263 | gst_message_parse_state_changed(message, ¤tState, &newState, nullptr); |
| 1264 | CString dotFileName = makeString(GST_OBJECT_NAME(m_pipeline.get()), '.', |
| 1265 | gst_element_state_get_name(currentState), '_', gst_element_state_get_name(newState)).utf8(); |
| 1266 | GST_DEBUG_BIN_TO_DOT_FILE_WITH_TS(GST_BIN(m_pipeline.get()), GST_DEBUG_GRAPH_SHOW_ALL, dotFileName.data()); |
| 1267 | |
| 1268 | break; |
| 1269 | } |
| 1270 | case GST_MESSAGE_BUFFERING: |
| 1271 | processBufferingStats(message); |
| 1272 | break; |
| 1273 | case GST_MESSAGE_DURATION_CHANGED: |
| 1274 | // Duration in MSE is managed by MediaSource, SourceBuffer and AppendPipeline. |
| 1275 | if (messageSourceIsPlaybin && !isMediaSource()) |
| 1276 | durationChanged(); |
| 1277 | break; |
| 1278 | case GST_MESSAGE_REQUEST_STATE: |
| 1279 | gst_message_parse_request_state(message, &requestedState); |
| 1280 | gst_element_get_state(m_pipeline.get(), ¤tState, nullptr, 250 * GST_NSECOND); |
| 1281 | if (requestedState < currentState) { |
| 1282 | GST_INFO_OBJECT(pipeline(), "Element %s requested state change to %s" , GST_MESSAGE_SRC_NAME(message), |
| 1283 | gst_element_state_get_name(requestedState)); |
| 1284 | m_requestedState = requestedState; |
| 1285 | if (!changePipelineState(requestedState)) |
| 1286 | loadingFailed(MediaPlayer::Empty); |
| 1287 | } |
| 1288 | break; |
| 1289 | case GST_MESSAGE_CLOCK_LOST: |
| 1290 | // This can only happen in PLAYING state and we should just |
| 1291 | // get a new clock by moving back to PAUSED and then to |
| 1292 | // PLAYING again. |
| 1293 | // This can happen if the stream that ends in a sink that |
| 1294 | // provides the current clock disappears, for example if |
| 1295 | // the audio sink provides the clock and the audio stream |
| 1296 | // is disabled. It also happens relatively often with |
| 1297 | // HTTP adaptive streams when switching between different |
| 1298 | // variants of a stream. |
| 1299 | gst_element_set_state(m_pipeline.get(), GST_STATE_PAUSED); |
| 1300 | gst_element_set_state(m_pipeline.get(), GST_STATE_PLAYING); |
| 1301 | break; |
| 1302 | case GST_MESSAGE_LATENCY: |
| 1303 | // Recalculate the latency, we don't need any special handling |
| 1304 | // here other than the GStreamer default. |
| 1305 | // This can happen if the latency of live elements changes, or |
| 1306 | // for one reason or another a new live element is added or |
| 1307 | // removed from the pipeline. |
| 1308 | gst_bin_recalculate_latency(GST_BIN(m_pipeline.get())); |
| 1309 | break; |
| 1310 | case GST_MESSAGE_ELEMENT: |
| 1311 | if (gst_is_missing_plugin_message(message)) { |
| 1312 | if (gst_install_plugins_supported()) { |
| 1313 | auto missingPluginCallback = MediaPlayerRequestInstallMissingPluginsCallback::create([weakThis = makeWeakPtr(*this)](uint32_t result, MediaPlayerRequestInstallMissingPluginsCallback& missingPluginCallback) { |
| 1314 | if (!weakThis) { |
| 1315 | GST_INFO("got missing pluging installation callback in destroyed player with result %u" , result); |
| 1316 | return; |
| 1317 | } |
| 1318 | |
| 1319 | GST_DEBUG("got missing plugin installation callback with result %u" , result); |
| 1320 | RefPtr<MediaPlayerRequestInstallMissingPluginsCallback> protectedMissingPluginCallback = &missingPluginCallback; |
| 1321 | weakThis->m_missingPluginCallbacks.removeFirst(protectedMissingPluginCallback); |
| 1322 | if (result != GST_INSTALL_PLUGINS_SUCCESS) |
| 1323 | return; |
| 1324 | |
| 1325 | weakThis->changePipelineState(GST_STATE_READY); |
| 1326 | weakThis->changePipelineState(GST_STATE_PAUSED); |
| 1327 | }); |
| 1328 | m_missingPluginCallbacks.append(missingPluginCallback.copyRef()); |
| 1329 | GUniquePtr<char> detail(gst_missing_plugin_message_get_installer_detail(message)); |
| 1330 | GUniquePtr<char> description(gst_missing_plugin_message_get_description(message)); |
| 1331 | m_player->client().requestInstallMissingPlugins(String::fromUTF8(detail.get()), String::fromUTF8(description.get()), missingPluginCallback.get()); |
| 1332 | } |
| 1333 | } |
| 1334 | #if ENABLE(VIDEO_TRACK) && USE(GSTREAMER_MPEGTS) |
| 1335 | else if (GstMpegtsSection* section = gst_message_parse_mpegts_section(message)) { |
| 1336 | processMpegTsSection(section); |
| 1337 | gst_mpegts_section_unref(section); |
| 1338 | } |
| 1339 | #endif |
| 1340 | #if ENABLE(ENCRYPTED_MEDIA) |
| 1341 | else if (gst_structure_has_name(structure, "drm-waiting-for-key" )) { |
| 1342 | GST_DEBUG_OBJECT(pipeline(), "drm-waiting-for-key message from %s" , GST_MESSAGE_SRC_NAME(message)); |
| 1343 | setWaitingForKey(true); |
| 1344 | // FIXME: The decryptors should be able to attempt to decrypt after being created and linked in a pipeline but currently they are not and current |
| 1345 | // architecture does not make this very easy. Fortunately, the arch will change soon and it does not pay off to fix this now with something that could be |
| 1346 | // more convoluted. In the meantime, force attempt to decrypt when they get blocked. |
| 1347 | attemptToDecryptWithLocalInstance(); |
| 1348 | } else if (gst_structure_has_name(structure, "drm-key-received" )) { |
| 1349 | GST_DEBUG_OBJECT(pipeline(), "drm-key-received message from %s" , GST_MESSAGE_SRC_NAME(message)); |
| 1350 | setWaitingForKey(false); |
| 1351 | } |
| 1352 | #endif |
| 1353 | else if (gst_structure_has_name(structure, "http-headers" )) { |
| 1354 | GST_DEBUG_OBJECT(pipeline(), "Processing HTTP headers: %" GST_PTR_FORMAT, structure); |
| 1355 | if (const char* uri = gst_structure_get_string(structure, "uri" )) { |
| 1356 | URL url(URL(), uri); |
| 1357 | convertToInternalProtocol(url); |
| 1358 | m_origins.add(SecurityOrigin::create(url)); |
| 1359 | |
| 1360 | if (url != m_url) { |
| 1361 | GST_DEBUG_OBJECT(pipeline(), "Ignoring HTTP response headers for non-main URI." ); |
| 1362 | break; |
| 1363 | } |
| 1364 | } |
| 1365 | GUniqueOutPtr<GstStructure> ; |
| 1366 | if (gst_structure_get(structure, "response-headers" , GST_TYPE_STRUCTURE, &responseHeaders.outPtr(), nullptr)) { |
| 1367 | const char* = httpHeaderNameString(HTTPHeaderName::ContentLength).utf8().data(); |
| 1368 | uint64_t contentLength = 0; |
| 1369 | if (!gst_structure_get_uint64(responseHeaders.get(), contentLengthHeaderName, &contentLength)) { |
| 1370 | // souphttpsrc sets a string for Content-Length, so |
| 1371 | // handle it here, until we remove the webkit+ protocol |
| 1372 | // prefix from webkitwebsrc. |
| 1373 | if (const char* contentLengthAsString = gst_structure_get_string(responseHeaders.get(), contentLengthHeaderName)) { |
| 1374 | contentLength = g_ascii_strtoull(contentLengthAsString, nullptr, 10); |
| 1375 | if (contentLength == G_MAXUINT64) |
| 1376 | contentLength = 0; |
| 1377 | } |
| 1378 | } |
| 1379 | GST_INFO_OBJECT(pipeline(), "%s stream detected" , !contentLength ? "Live" : "Non-live" ); |
| 1380 | if (!contentLength) { |
| 1381 | m_isStreaming = true; |
| 1382 | setDownloadBuffering(); |
| 1383 | } |
| 1384 | } |
| 1385 | } else if (gst_structure_has_name(structure, "webkit-network-statistics" )) { |
| 1386 | if (gst_structure_get(structure, "read-position" , G_TYPE_UINT64, &m_networkReadPosition, "size" , G_TYPE_UINT64, &m_httpResponseTotalSize, nullptr)) |
| 1387 | GST_DEBUG_OBJECT(pipeline(), "Updated network read position %" G_GUINT64_FORMAT ", size: %" G_GUINT64_FORMAT, m_networkReadPosition, m_httpResponseTotalSize); |
| 1388 | } else if (gst_structure_has_name(structure, "adaptive-streaming-statistics" )) { |
| 1389 | if (WEBKIT_IS_WEB_SRC(m_source.get()) && !webkitGstCheckVersion(1, 12, 0)) { |
| 1390 | if (const char* uri = gst_structure_get_string(structure, "uri" )) |
| 1391 | m_hasTaintedOrigin = webKitSrcWouldTaintOrigin(WEBKIT_WEB_SRC(m_source.get()), SecurityOrigin::create(URL(URL(), uri))); |
| 1392 | } |
| 1393 | } else |
| 1394 | GST_DEBUG_OBJECT(pipeline(), "Unhandled element message: %" GST_PTR_FORMAT, structure); |
| 1395 | break; |
| 1396 | #if ENABLE(VIDEO_TRACK) |
| 1397 | case GST_MESSAGE_TOC: |
| 1398 | processTableOfContents(message); |
| 1399 | break; |
| 1400 | #endif |
| 1401 | case GST_MESSAGE_TAG: { |
| 1402 | GstTagList* tags = nullptr; |
| 1403 | GUniqueOutPtr<gchar> tag; |
| 1404 | gst_message_parse_tag(message, &tags); |
| 1405 | if (gst_tag_list_get_string(tags, GST_TAG_IMAGE_ORIENTATION, &tag.outPtr())) { |
| 1406 | if (!g_strcmp0(tag.get(), "rotate-90" )) |
| 1407 | setVideoSourceOrientation(ImageOrientation(OriginRightTop)); |
| 1408 | else if (!g_strcmp0(tag.get(), "rotate-180" )) |
| 1409 | setVideoSourceOrientation(ImageOrientation(OriginBottomRight)); |
| 1410 | else if (!g_strcmp0(tag.get(), "rotate-270" )) |
| 1411 | setVideoSourceOrientation(ImageOrientation(OriginLeftBottom)); |
| 1412 | } |
| 1413 | gst_tag_list_unref(tags); |
| 1414 | break; |
| 1415 | } |
| 1416 | #if GST_CHECK_VERSION(1, 10, 0) |
| 1417 | case GST_MESSAGE_STREAMS_SELECTED: { |
| 1418 | GRefPtr<GstStreamCollection> collection; |
| 1419 | gst_message_parse_streams_selected(message, &collection.outPtr()); |
| 1420 | |
| 1421 | if (!collection) |
| 1422 | break; |
| 1423 | |
| 1424 | m_streamCollection.swap(collection); |
| 1425 | m_currentAudioStreamId = "" ; |
| 1426 | m_currentVideoStreamId = "" ; |
| 1427 | m_currentTextStreamId = "" ; |
| 1428 | |
| 1429 | unsigned length = gst_message_streams_selected_get_size(message); |
| 1430 | for (unsigned i = 0; i < length; i++) { |
| 1431 | GRefPtr<GstStream> stream = gst_message_streams_selected_get_stream(message, i); |
| 1432 | if (!stream) |
| 1433 | continue; |
| 1434 | |
| 1435 | GstStreamType type = gst_stream_get_stream_type(stream.get()); |
| 1436 | String streamId(gst_stream_get_stream_id(stream.get())); |
| 1437 | |
| 1438 | GST_DEBUG_OBJECT(pipeline(), "Selecting %s track with ID: %s" , gst_stream_type_get_name(type), streamId.utf8().data()); |
| 1439 | // Playbin3 can send more than one selected stream of the same type |
| 1440 | // but there's no priority or ordering system in place, so we assume |
| 1441 | // the selected stream is the last one as reported by playbin3. |
| 1442 | if (type & GST_STREAM_TYPE_AUDIO) { |
| 1443 | m_currentAudioStreamId = streamId; |
| 1444 | auto track = m_audioTracks.get(m_currentAudioStreamId); |
| 1445 | ASSERT(track); |
| 1446 | track->markAsActive(); |
| 1447 | } else if (type & GST_STREAM_TYPE_VIDEO) { |
| 1448 | m_currentVideoStreamId = streamId; |
| 1449 | auto track = m_videoTracks.get(m_currentVideoStreamId); |
| 1450 | ASSERT(track); |
| 1451 | track->markAsActive(); |
| 1452 | } else if (type & GST_STREAM_TYPE_TEXT) |
| 1453 | m_currentTextStreamId = streamId; |
| 1454 | else |
| 1455 | GST_WARNING("Unknown stream type with stream-id %s" , streamId.utf8().data()); |
| 1456 | } |
| 1457 | break; |
| 1458 | } |
| 1459 | #endif |
| 1460 | default: |
| 1461 | GST_DEBUG_OBJECT(pipeline(), "Unhandled GStreamer message type: %s" , GST_MESSAGE_TYPE_NAME(message)); |
| 1462 | break; |
| 1463 | } |
| 1464 | } |
| 1465 | |
| 1466 | void MediaPlayerPrivateGStreamer::processBufferingStats(GstMessage* message) |
| 1467 | { |
| 1468 | m_buffering = true; |
| 1469 | gst_message_parse_buffering(message, &m_bufferingPercentage); |
| 1470 | |
| 1471 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Buffering: %d%%." , m_bufferingPercentage); |
| 1472 | |
| 1473 | if (m_bufferingPercentage == 100) |
| 1474 | updateStates(); |
| 1475 | } |
| 1476 | |
| 1477 | #if ENABLE(VIDEO_TRACK) && USE(GSTREAMER_MPEGTS) |
| 1478 | void MediaPlayerPrivateGStreamer::processMpegTsSection(GstMpegtsSection* section) |
| 1479 | { |
| 1480 | ASSERT(section); |
| 1481 | |
| 1482 | if (section->section_type == GST_MPEGTS_SECTION_PMT) { |
| 1483 | const GstMpegtsPMT* pmt = gst_mpegts_section_get_pmt(section); |
| 1484 | m_metadataTracks.clear(); |
| 1485 | for (guint i = 0; i < pmt->streams->len; ++i) { |
| 1486 | const GstMpegtsPMTStream* stream = static_cast<const GstMpegtsPMTStream*>(g_ptr_array_index(pmt->streams, i)); |
| 1487 | if (stream->stream_type == 0x05 || stream->stream_type >= 0x80) { |
| 1488 | AtomicString pid = String::number(stream->pid); |
| 1489 | auto track = InbandMetadataTextTrackPrivateGStreamer::create( |
| 1490 | InbandTextTrackPrivate::Metadata, InbandTextTrackPrivate::Data, pid); |
| 1491 | |
| 1492 | // 4.7.10.12.2 Sourcing in-band text tracks |
| 1493 | // If the new text track's kind is metadata, then set the text track in-band metadata track dispatch |
| 1494 | // type as follows, based on the type of the media resource: |
| 1495 | // Let stream type be the value of the "stream_type" field describing the text track's type in the |
| 1496 | // file's program map section, interpreted as an 8-bit unsigned integer. Let length be the value of |
| 1497 | // the "ES_info_length" field for the track in the same part of the program map section, interpreted |
| 1498 | // as an integer as defined by the MPEG-2 specification. Let descriptor bytes be the length bytes |
| 1499 | // following the "ES_info_length" field. The text track in-band metadata track dispatch type must be |
| 1500 | // set to the concatenation of the stream type byte and the zero or more descriptor bytes bytes, |
| 1501 | // expressed in hexadecimal using uppercase ASCII hex digits. |
| 1502 | String inbandMetadataTrackDispatchType; |
| 1503 | appendUnsignedAsHexFixedSize(stream->stream_type, inbandMetadataTrackDispatchType, 2); |
| 1504 | for (guint j = 0; j < stream->descriptors->len; ++j) { |
| 1505 | const GstMpegtsDescriptor* descriptor = static_cast<const GstMpegtsDescriptor*>(g_ptr_array_index(stream->descriptors, j)); |
| 1506 | for (guint k = 0; k < descriptor->length; ++k) |
| 1507 | appendByteAsHex(descriptor->data[k], inbandMetadataTrackDispatchType); |
| 1508 | } |
| 1509 | track->setInBandMetadataTrackDispatchType(inbandMetadataTrackDispatchType); |
| 1510 | |
| 1511 | m_metadataTracks.add(pid, track); |
| 1512 | m_player->addTextTrack(*track); |
| 1513 | } |
| 1514 | } |
| 1515 | } else { |
| 1516 | AtomicString pid = String::number(section->pid); |
| 1517 | RefPtr<InbandMetadataTextTrackPrivateGStreamer> track = m_metadataTracks.get(pid); |
| 1518 | if (!track) |
| 1519 | return; |
| 1520 | |
| 1521 | GRefPtr<GBytes> data = gst_mpegts_section_get_data(section); |
| 1522 | gsize size; |
| 1523 | const void* bytes = g_bytes_get_data(data.get(), &size); |
| 1524 | |
| 1525 | track->addDataCue(currentMediaTime(), currentMediaTime(), bytes, size); |
| 1526 | } |
| 1527 | } |
| 1528 | #endif |
| 1529 | |
| 1530 | #if ENABLE(VIDEO_TRACK) |
| 1531 | void MediaPlayerPrivateGStreamer::processTableOfContents(GstMessage* message) |
| 1532 | { |
| 1533 | if (m_chaptersTrack) |
| 1534 | m_player->removeTextTrack(*m_chaptersTrack); |
| 1535 | |
| 1536 | m_chaptersTrack = InbandMetadataTextTrackPrivateGStreamer::create(InbandTextTrackPrivate::Chapters, InbandTextTrackPrivate::Generic); |
| 1537 | m_player->addTextTrack(*m_chaptersTrack); |
| 1538 | |
| 1539 | GRefPtr<GstToc> toc; |
| 1540 | gboolean updated; |
| 1541 | gst_message_parse_toc(message, &toc.outPtr(), &updated); |
| 1542 | ASSERT(toc); |
| 1543 | |
| 1544 | for (GList* i = gst_toc_get_entries(toc.get()); i; i = i->next) |
| 1545 | processTableOfContentsEntry(static_cast<GstTocEntry*>(i->data)); |
| 1546 | } |
| 1547 | |
| 1548 | void MediaPlayerPrivateGStreamer::processTableOfContentsEntry(GstTocEntry* entry) |
| 1549 | { |
| 1550 | ASSERT(entry); |
| 1551 | |
| 1552 | auto cue = GenericCueData::create(); |
| 1553 | |
| 1554 | gint64 start = -1, stop = -1; |
| 1555 | gst_toc_entry_get_start_stop_times(entry, &start, &stop); |
| 1556 | if (start != -1) |
| 1557 | cue->setStartTime(MediaTime(start, GST_SECOND)); |
| 1558 | if (stop != -1) |
| 1559 | cue->setEndTime(MediaTime(stop, GST_SECOND)); |
| 1560 | |
| 1561 | GstTagList* tags = gst_toc_entry_get_tags(entry); |
| 1562 | if (tags) { |
| 1563 | gchar* title = nullptr; |
| 1564 | gst_tag_list_get_string(tags, GST_TAG_TITLE, &title); |
| 1565 | if (title) { |
| 1566 | cue->setContent(title); |
| 1567 | g_free(title); |
| 1568 | } |
| 1569 | } |
| 1570 | |
| 1571 | m_chaptersTrack->addGenericCue(cue); |
| 1572 | |
| 1573 | for (GList* i = gst_toc_entry_get_sub_entries(entry); i; i = i->next) |
| 1574 | processTableOfContentsEntry(static_cast<GstTocEntry*>(i->data)); |
| 1575 | } |
| 1576 | |
| 1577 | void MediaPlayerPrivateGStreamer::purgeInvalidAudioTracks(Vector<String> validTrackIds) |
| 1578 | { |
| 1579 | m_audioTracks.removeIf([validTrackIds](auto& keyAndValue) { |
| 1580 | return !validTrackIds.contains(keyAndValue.key); |
| 1581 | }); |
| 1582 | } |
| 1583 | |
| 1584 | void MediaPlayerPrivateGStreamer::purgeInvalidVideoTracks(Vector<String> validTrackIds) |
| 1585 | { |
| 1586 | m_videoTracks.removeIf([validTrackIds](auto& keyAndValue) { |
| 1587 | return !validTrackIds.contains(keyAndValue.key); |
| 1588 | }); |
| 1589 | } |
| 1590 | |
| 1591 | void MediaPlayerPrivateGStreamer::purgeInvalidTextTracks(Vector<String> validTrackIds) |
| 1592 | { |
| 1593 | m_textTracks.removeIf([validTrackIds](auto& keyAndValue) { |
| 1594 | return !validTrackIds.contains(keyAndValue.key); |
| 1595 | }); |
| 1596 | } |
| 1597 | #endif |
| 1598 | |
| 1599 | void MediaPlayerPrivateGStreamer::fillTimerFired() |
| 1600 | { |
| 1601 | GRefPtr<GstQuery> query = adoptGRef(gst_query_new_buffering(GST_FORMAT_PERCENT)); |
| 1602 | double fillStatus = 100.0; |
| 1603 | |
| 1604 | if (gst_element_query(m_pipeline.get(), query.get())) { |
| 1605 | int64_t stop; |
| 1606 | GstFormat format; |
| 1607 | gst_query_parse_buffering_range(query.get(), &format, nullptr, &stop, nullptr); |
| 1608 | ASSERT(format == GST_FORMAT_PERCENT); |
| 1609 | |
| 1610 | if (stop != -1) |
| 1611 | fillStatus = 100.0 * stop / GST_FORMAT_PERCENT_MAX; |
| 1612 | } else if (m_httpResponseTotalSize) { |
| 1613 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Query failed, falling back to network read position estimation" ); |
| 1614 | fillStatus = 100.0 * (m_networkReadPosition / m_httpResponseTotalSize); |
| 1615 | } else { |
| 1616 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Unable to determine on-disk buffering status" ); |
| 1617 | return; |
| 1618 | } |
| 1619 | |
| 1620 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Download buffer filled up to %f%%" , fillStatus); |
| 1621 | |
| 1622 | MediaTime mediaDuration = durationMediaTime(); |
| 1623 | |
| 1624 | // Update maxTimeLoaded only if the media duration is |
| 1625 | // available. Otherwise we can't compute it. |
| 1626 | if (mediaDuration) { |
| 1627 | if (fillStatus == 100.0) |
| 1628 | m_maxTimeLoaded = mediaDuration; |
| 1629 | else |
| 1630 | m_maxTimeLoaded = MediaTime(fillStatus * static_cast<double>(toGstUnsigned64Time(mediaDuration)) / 100, GST_SECOND); |
| 1631 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Updated maxTimeLoaded: %s" , toString(m_maxTimeLoaded).utf8().data()); |
| 1632 | } |
| 1633 | |
| 1634 | m_downloadFinished = fillStatus == 100.0; |
| 1635 | if (!m_downloadFinished) { |
| 1636 | updateStates(); |
| 1637 | return; |
| 1638 | } |
| 1639 | |
| 1640 | // Media is now fully loaded. It will play even if network |
| 1641 | // connection is cut. Buffering is done, remove the fill source |
| 1642 | // from the main loop. |
| 1643 | m_fillTimer.stop(); |
| 1644 | updateStates(); |
| 1645 | } |
| 1646 | |
| 1647 | MediaTime MediaPlayerPrivateGStreamer::maxMediaTimeSeekable() const |
| 1648 | { |
| 1649 | GST_TRACE_OBJECT(pipeline(), "errorOccured: %s, isLiveStream: %s" , boolForPrinting(m_errorOccured), boolForPrinting(isLiveStream())); |
| 1650 | if (m_errorOccured) |
| 1651 | return MediaTime::zeroTime(); |
| 1652 | |
| 1653 | if (isLiveStream()) |
| 1654 | return MediaTime::zeroTime(); |
| 1655 | |
| 1656 | MediaTime duration = durationMediaTime(); |
| 1657 | GST_DEBUG_OBJECT(pipeline(), "maxMediaTimeSeekable, duration: %s" , toString(duration).utf8().data()); |
| 1658 | // infinite duration means live stream |
| 1659 | if (duration.isPositiveInfinite()) |
| 1660 | return MediaTime::zeroTime(); |
| 1661 | |
| 1662 | return duration; |
| 1663 | } |
| 1664 | |
| 1665 | MediaTime MediaPlayerPrivateGStreamer::maxTimeLoaded() const |
| 1666 | { |
| 1667 | if (m_errorOccured) |
| 1668 | return MediaTime::zeroTime(); |
| 1669 | |
| 1670 | MediaTime loaded = m_maxTimeLoaded; |
| 1671 | if (m_isEndReached) |
| 1672 | loaded = durationMediaTime(); |
| 1673 | GST_LOG("maxTimeLoaded: %s" , toString(loaded).utf8().data()); |
| 1674 | return loaded; |
| 1675 | } |
| 1676 | |
| 1677 | bool MediaPlayerPrivateGStreamer::didLoadingProgress() const |
| 1678 | { |
| 1679 | if (m_errorOccured || m_loadingStalled) |
| 1680 | return false; |
| 1681 | |
| 1682 | if (WEBKIT_IS_WEB_SRC(m_source.get())) { |
| 1683 | GST_LOG_OBJECT(pipeline(), "Last network read position: %" G_GUINT64_FORMAT ", current: %" G_GUINT64_FORMAT, m_readPositionAtLastDidLoadingProgress, m_networkReadPosition); |
| 1684 | bool didLoadingProgress = m_readPositionAtLastDidLoadingProgress != m_networkReadPosition; |
| 1685 | m_readPositionAtLastDidLoadingProgress = m_networkReadPosition; |
| 1686 | return didLoadingProgress; |
| 1687 | } |
| 1688 | |
| 1689 | if (UNLIKELY(!m_pipeline || !durationMediaTime() || (!isMediaSource() && !totalBytes()))) |
| 1690 | return false; |
| 1691 | |
| 1692 | MediaTime currentMaxTimeLoaded = maxTimeLoaded(); |
| 1693 | bool didLoadingProgress = currentMaxTimeLoaded != m_maxTimeLoadedAtLastDidLoadingProgress; |
| 1694 | m_maxTimeLoadedAtLastDidLoadingProgress = currentMaxTimeLoaded; |
| 1695 | GST_LOG_OBJECT(pipeline(), "didLoadingProgress: %s" , boolForPrinting(didLoadingProgress)); |
| 1696 | return didLoadingProgress; |
| 1697 | } |
| 1698 | |
| 1699 | unsigned long long MediaPlayerPrivateGStreamer::totalBytes() const |
| 1700 | { |
| 1701 | if (m_errorOccured) |
| 1702 | return 0; |
| 1703 | |
| 1704 | if (m_totalBytes) |
| 1705 | return m_totalBytes; |
| 1706 | |
| 1707 | if (!m_source) |
| 1708 | return 0; |
| 1709 | |
| 1710 | if (isLiveStream()) |
| 1711 | return 0; |
| 1712 | |
| 1713 | GstFormat fmt = GST_FORMAT_BYTES; |
| 1714 | gint64 length = 0; |
| 1715 | if (gst_element_query_duration(m_source.get(), fmt, &length)) { |
| 1716 | GST_INFO_OBJECT(pipeline(), "totalBytes %" G_GINT64_FORMAT, length); |
| 1717 | m_totalBytes = static_cast<unsigned long long>(length); |
| 1718 | m_isStreaming = !length; |
| 1719 | return m_totalBytes; |
| 1720 | } |
| 1721 | |
| 1722 | // Fall back to querying the source pads manually. |
| 1723 | // See also https://bugzilla.gnome.org/show_bug.cgi?id=638749 |
| 1724 | GstIterator* iter = gst_element_iterate_src_pads(m_source.get()); |
| 1725 | bool done = false; |
| 1726 | while (!done) { |
| 1727 | GValue item = G_VALUE_INIT; |
| 1728 | switch (gst_iterator_next(iter, &item)) { |
| 1729 | case GST_ITERATOR_OK: { |
| 1730 | GstPad* pad = static_cast<GstPad*>(g_value_get_object(&item)); |
| 1731 | gint64 padLength = 0; |
| 1732 | if (gst_pad_query_duration(pad, fmt, &padLength) && padLength > length) |
| 1733 | length = padLength; |
| 1734 | break; |
| 1735 | } |
| 1736 | case GST_ITERATOR_RESYNC: |
| 1737 | gst_iterator_resync(iter); |
| 1738 | break; |
| 1739 | case GST_ITERATOR_ERROR: |
| 1740 | FALLTHROUGH; |
| 1741 | case GST_ITERATOR_DONE: |
| 1742 | done = true; |
| 1743 | break; |
| 1744 | } |
| 1745 | |
| 1746 | g_value_unset(&item); |
| 1747 | } |
| 1748 | |
| 1749 | gst_iterator_free(iter); |
| 1750 | |
| 1751 | GST_INFO_OBJECT(pipeline(), "totalBytes %" G_GINT64_FORMAT, length); |
| 1752 | m_totalBytes = static_cast<unsigned long long>(length); |
| 1753 | m_isStreaming = !length; |
| 1754 | return m_totalBytes; |
| 1755 | } |
| 1756 | |
| 1757 | void MediaPlayerPrivateGStreamer::sourceSetupCallback(MediaPlayerPrivateGStreamer* player, GstElement* sourceElement) |
| 1758 | { |
| 1759 | player->sourceSetup(sourceElement); |
| 1760 | } |
| 1761 | |
| 1762 | void MediaPlayerPrivateGStreamer::uriDecodeBinElementAddedCallback(GstBin* bin, GstElement* element, MediaPlayerPrivateGStreamer* player) |
| 1763 | { |
| 1764 | if (g_strcmp0(G_OBJECT_TYPE_NAME(element), "GstDownloadBuffer" )) |
| 1765 | return; |
| 1766 | |
| 1767 | player->m_downloadBuffer = element; |
| 1768 | g_signal_handlers_disconnect_by_func(bin, reinterpret_cast<gpointer>(uriDecodeBinElementAddedCallback), player); |
| 1769 | g_signal_connect_swapped(element, "notify::temp-location" , G_CALLBACK(downloadBufferFileCreatedCallback), player); |
| 1770 | |
| 1771 | GUniqueOutPtr<char> oldDownloadTemplate; |
| 1772 | g_object_get(element, "temp-template" , &oldDownloadTemplate.outPtr(), nullptr); |
| 1773 | |
| 1774 | GUniquePtr<char> newDownloadTemplate(g_build_filename(G_DIR_SEPARATOR_S, "var" , "tmp" , "WebKit-Media-XXXXXX" , nullptr)); |
| 1775 | g_object_set(element, "temp-template" , newDownloadTemplate.get(), nullptr); |
| 1776 | GST_DEBUG_OBJECT(player->pipeline(), "Reconfigured file download template from '%s' to '%s'" , oldDownloadTemplate.get(), newDownloadTemplate.get()); |
| 1777 | |
| 1778 | player->purgeOldDownloadFiles(oldDownloadTemplate.get()); |
| 1779 | } |
| 1780 | |
| 1781 | void MediaPlayerPrivateGStreamer::downloadBufferFileCreatedCallback(MediaPlayerPrivateGStreamer* player) |
| 1782 | { |
| 1783 | ASSERT(player->m_downloadBuffer); |
| 1784 | |
| 1785 | g_signal_handlers_disconnect_by_func(player->m_downloadBuffer.get(), reinterpret_cast<gpointer>(downloadBufferFileCreatedCallback), player); |
| 1786 | |
| 1787 | GUniqueOutPtr<char> downloadFile; |
| 1788 | g_object_get(player->m_downloadBuffer.get(), "temp-location" , &downloadFile.outPtr(), nullptr); |
| 1789 | player->m_downloadBuffer = nullptr; |
| 1790 | |
| 1791 | if (UNLIKELY(!FileSystem::deleteFile(downloadFile.get()))) { |
| 1792 | GST_WARNING("Couldn't unlink media temporary file %s after creation" , downloadFile.get()); |
| 1793 | return; |
| 1794 | } |
| 1795 | |
| 1796 | GST_DEBUG_OBJECT(player->pipeline(), "Unlinked media temporary file %s after creation" , downloadFile.get()); |
| 1797 | } |
| 1798 | |
| 1799 | void MediaPlayerPrivateGStreamer::purgeOldDownloadFiles(const char* downloadFileTemplate) |
| 1800 | { |
| 1801 | if (!downloadFileTemplate) |
| 1802 | return; |
| 1803 | |
| 1804 | GUniquePtr<char> templatePath(g_path_get_dirname(downloadFileTemplate)); |
| 1805 | GUniquePtr<char> templateFile(g_path_get_basename(downloadFileTemplate)); |
| 1806 | String templatePattern = String(templateFile.get()).replace("X" , "?" ); |
| 1807 | |
| 1808 | for (auto& filePath : FileSystem::listDirectory(templatePath.get(), templatePattern)) { |
| 1809 | if (UNLIKELY(!FileSystem::deleteFile(filePath))) { |
| 1810 | GST_WARNING("Couldn't unlink legacy media temporary file: %s" , filePath.utf8().data()); |
| 1811 | continue; |
| 1812 | } |
| 1813 | |
| 1814 | GST_TRACE("Unlinked legacy media temporary file: %s" , filePath.utf8().data()); |
| 1815 | } |
| 1816 | } |
| 1817 | |
| 1818 | void MediaPlayerPrivateGStreamer::sourceSetup(GstElement* sourceElement) |
| 1819 | { |
| 1820 | GST_DEBUG_OBJECT(pipeline(), "Source element set-up for %s" , GST_ELEMENT_NAME(sourceElement)); |
| 1821 | |
| 1822 | if (WEBKIT_IS_WEB_SRC(m_source.get()) && GST_OBJECT_PARENT(m_source.get())) |
| 1823 | g_signal_handlers_disconnect_by_func(GST_ELEMENT_PARENT(m_source.get()), reinterpret_cast<gpointer>(uriDecodeBinElementAddedCallback), this); |
| 1824 | |
| 1825 | m_source = sourceElement; |
| 1826 | |
| 1827 | if (WEBKIT_IS_WEB_SRC(m_source.get())) { |
| 1828 | webKitWebSrcSetMediaPlayer(WEBKIT_WEB_SRC(m_source.get()), m_player); |
| 1829 | g_signal_connect(GST_ELEMENT_PARENT(m_source.get()), "element-added" , G_CALLBACK(uriDecodeBinElementAddedCallback), this); |
| 1830 | #if ENABLE(MEDIA_STREAM) && GST_CHECK_VERSION(1, 10, 0) |
| 1831 | } else if (WEBKIT_IS_MEDIA_STREAM_SRC(sourceElement)) { |
| 1832 | auto stream = m_streamPrivate.get(); |
| 1833 | ASSERT(stream); |
| 1834 | webkitMediaStreamSrcSetStream(WEBKIT_MEDIA_STREAM_SRC(sourceElement), stream); |
| 1835 | #endif |
| 1836 | } |
| 1837 | } |
| 1838 | |
| 1839 | bool MediaPlayerPrivateGStreamer::hasSingleSecurityOrigin() const |
| 1840 | { |
| 1841 | if (!m_source) |
| 1842 | return false; |
| 1843 | |
| 1844 | if (!WEBKIT_IS_WEB_SRC(m_source.get())) |
| 1845 | return true; |
| 1846 | |
| 1847 | GUniqueOutPtr<char> originalURI, resolvedURI; |
| 1848 | g_object_get(m_source.get(), "location" , &originalURI.outPtr(), "resolved-location" , &resolvedURI.outPtr(), nullptr); |
| 1849 | if (!originalURI || !resolvedURI) |
| 1850 | return false; |
| 1851 | if (!g_strcmp0(originalURI.get(), resolvedURI.get())) |
| 1852 | return true; |
| 1853 | |
| 1854 | Ref<SecurityOrigin> resolvedOrigin(SecurityOrigin::createFromString(String::fromUTF8(resolvedURI.get()))); |
| 1855 | Ref<SecurityOrigin> requestedOrigin(SecurityOrigin::createFromString(String::fromUTF8(originalURI.get()))); |
| 1856 | return resolvedOrigin->isSameSchemeHostPort(requestedOrigin.get()); |
| 1857 | } |
| 1858 | |
| 1859 | void MediaPlayerPrivateGStreamer::cancelLoad() |
| 1860 | { |
| 1861 | if (m_networkState < MediaPlayer::Loading || m_networkState == MediaPlayer::Loaded) |
| 1862 | return; |
| 1863 | |
| 1864 | if (m_pipeline) |
| 1865 | changePipelineState(GST_STATE_READY); |
| 1866 | } |
| 1867 | |
| 1868 | void MediaPlayerPrivateGStreamer::asyncStateChangeDone() |
| 1869 | { |
| 1870 | if (!m_pipeline || m_errorOccured) |
| 1871 | return; |
| 1872 | |
| 1873 | if (m_seeking) { |
| 1874 | if (m_seekIsPending) |
| 1875 | updateStates(); |
| 1876 | else { |
| 1877 | GST_DEBUG_OBJECT(pipeline(), "[Seek] seeked to %s" , toString(m_seekTime).utf8().data()); |
| 1878 | m_seeking = false; |
| 1879 | m_cachedPosition = MediaTime::invalidTime(); |
| 1880 | if (m_timeOfOverlappingSeek != m_seekTime && m_timeOfOverlappingSeek.isValid()) { |
| 1881 | seek(m_timeOfOverlappingSeek); |
| 1882 | m_timeOfOverlappingSeek = MediaTime::invalidTime(); |
| 1883 | return; |
| 1884 | } |
| 1885 | m_timeOfOverlappingSeek = MediaTime::invalidTime(); |
| 1886 | |
| 1887 | // The pipeline can still have a pending state. In this case a position query will fail. |
| 1888 | // Right now we can use m_seekTime as a fallback. |
| 1889 | m_canFallBackToLastFinishedSeekPosition = true; |
| 1890 | timeChanged(); |
| 1891 | } |
| 1892 | } else |
| 1893 | updateStates(); |
| 1894 | } |
| 1895 | |
| 1896 | void MediaPlayerPrivateGStreamer::updateStates() |
| 1897 | { |
| 1898 | if (!m_pipeline) |
| 1899 | return; |
| 1900 | |
| 1901 | if (m_errorOccured) |
| 1902 | return; |
| 1903 | |
| 1904 | MediaPlayer::NetworkState oldNetworkState = m_networkState; |
| 1905 | MediaPlayer::ReadyState oldReadyState = m_readyState; |
| 1906 | GstState pending; |
| 1907 | GstState state; |
| 1908 | bool stateReallyChanged = false; |
| 1909 | |
| 1910 | GstStateChangeReturn getStateResult = gst_element_get_state(m_pipeline.get(), &state, &pending, 250 * GST_NSECOND); |
| 1911 | if (state != m_currentState) { |
| 1912 | m_oldState = m_currentState; |
| 1913 | m_currentState = state; |
| 1914 | stateReallyChanged = true; |
| 1915 | } |
| 1916 | |
| 1917 | bool shouldUpdatePlaybackState = false; |
| 1918 | switch (getStateResult) { |
| 1919 | case GST_STATE_CHANGE_SUCCESS: { |
| 1920 | GST_DEBUG_OBJECT(pipeline(), "State: %s, pending: %s" , gst_element_state_get_name(m_currentState), gst_element_state_get_name(pending)); |
| 1921 | |
| 1922 | // Do nothing if on EOS and state changed to READY to avoid recreating the player |
| 1923 | // on HTMLMediaElement and properly generate the video 'ended' event. |
| 1924 | if (m_isEndReached && m_currentState == GST_STATE_READY) |
| 1925 | break; |
| 1926 | |
| 1927 | m_resetPipeline = m_currentState <= GST_STATE_READY; |
| 1928 | |
| 1929 | bool didBuffering = m_buffering; |
| 1930 | |
| 1931 | // Update ready and network states. |
| 1932 | switch (m_currentState) { |
| 1933 | case GST_STATE_NULL: |
| 1934 | m_readyState = MediaPlayer::HaveNothing; |
| 1935 | m_networkState = MediaPlayer::Empty; |
| 1936 | break; |
| 1937 | case GST_STATE_READY: |
| 1938 | m_readyState = MediaPlayer::HaveMetadata; |
| 1939 | m_networkState = MediaPlayer::Empty; |
| 1940 | break; |
| 1941 | case GST_STATE_PAUSED: |
| 1942 | case GST_STATE_PLAYING: |
| 1943 | if (m_buffering) { |
| 1944 | if (m_bufferingPercentage == 100) { |
| 1945 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Complete." ); |
| 1946 | m_buffering = false; |
| 1947 | m_readyState = MediaPlayer::HaveEnoughData; |
| 1948 | m_networkState = m_downloadFinished ? MediaPlayer::Idle : MediaPlayer::Loading; |
| 1949 | } else { |
| 1950 | m_readyState = MediaPlayer::HaveCurrentData; |
| 1951 | m_networkState = MediaPlayer::Loading; |
| 1952 | } |
| 1953 | } else if (m_downloadFinished) { |
| 1954 | m_readyState = MediaPlayer::HaveEnoughData; |
| 1955 | m_networkState = MediaPlayer::Loaded; |
| 1956 | } else { |
| 1957 | m_readyState = MediaPlayer::HaveFutureData; |
| 1958 | m_networkState = MediaPlayer::Loading; |
| 1959 | } |
| 1960 | |
| 1961 | break; |
| 1962 | default: |
| 1963 | ASSERT_NOT_REACHED(); |
| 1964 | break; |
| 1965 | } |
| 1966 | |
| 1967 | // Sync states where needed. |
| 1968 | if (m_currentState == GST_STATE_PAUSED) { |
| 1969 | if (!m_volumeAndMuteInitialized) { |
| 1970 | notifyPlayerOfVolumeChange(); |
| 1971 | notifyPlayerOfMute(); |
| 1972 | m_volumeAndMuteInitialized = true; |
| 1973 | } |
| 1974 | |
| 1975 | if (didBuffering && !m_buffering && !m_paused && m_playbackRate) { |
| 1976 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Restarting playback." ); |
| 1977 | changePipelineState(GST_STATE_PLAYING); |
| 1978 | } |
| 1979 | } else if (m_currentState == GST_STATE_PLAYING) { |
| 1980 | m_paused = false; |
| 1981 | |
| 1982 | if ((m_buffering && !isLiveStream()) || !m_playbackRate) { |
| 1983 | GST_DEBUG_OBJECT(pipeline(), "[Buffering] Pausing stream for buffering." ); |
| 1984 | changePipelineState(GST_STATE_PAUSED); |
| 1985 | } |
| 1986 | } else |
| 1987 | m_paused = true; |
| 1988 | |
| 1989 | GST_DEBUG_OBJECT(pipeline(), "Old state: %s, new state: %s (requested: %s)" , gst_element_state_get_name(m_oldState), gst_element_state_get_name(m_currentState), gst_element_state_get_name(m_requestedState)); |
| 1990 | if (m_requestedState == GST_STATE_PAUSED && m_currentState == GST_STATE_PAUSED) { |
| 1991 | shouldUpdatePlaybackState = true; |
| 1992 | GST_INFO_OBJECT(pipeline(), "Requested state change to %s was completed" , gst_element_state_get_name(m_currentState)); |
| 1993 | } |
| 1994 | |
| 1995 | // Emit play state change notification only when going to PLAYING so that |
| 1996 | // the media element gets a chance to enable its page sleep disabler. |
| 1997 | // Emitting this notification in more cases triggers unwanted code paths |
| 1998 | // and test timeouts. |
| 1999 | if (stateReallyChanged && (m_oldState != m_currentState) && (m_oldState == GST_STATE_PAUSED && m_currentState == GST_STATE_PLAYING)) { |
| 2000 | GST_INFO_OBJECT(pipeline(), "Playback state changed from %s to %s. Notifying the media player client" , gst_element_state_get_name(m_oldState), gst_element_state_get_name(m_currentState)); |
| 2001 | shouldUpdatePlaybackState = true; |
| 2002 | } |
| 2003 | |
| 2004 | break; |
| 2005 | } |
| 2006 | case GST_STATE_CHANGE_ASYNC: |
| 2007 | GST_DEBUG_OBJECT(pipeline(), "Async: State: %s, pending: %s" , gst_element_state_get_name(m_currentState), gst_element_state_get_name(pending)); |
| 2008 | // Change in progress. |
| 2009 | break; |
| 2010 | case GST_STATE_CHANGE_FAILURE: |
| 2011 | GST_DEBUG_OBJECT(pipeline(), "Failure: State: %s, pending: %s" , gst_element_state_get_name(m_currentState), gst_element_state_get_name(pending)); |
| 2012 | // Change failed |
| 2013 | return; |
| 2014 | case GST_STATE_CHANGE_NO_PREROLL: |
| 2015 | GST_DEBUG_OBJECT(pipeline(), "No preroll: State: %s, pending: %s" , gst_element_state_get_name(m_currentState), gst_element_state_get_name(pending)); |
| 2016 | |
| 2017 | // Live pipelines go in PAUSED without prerolling. |
| 2018 | m_isStreaming = true; |
| 2019 | setDownloadBuffering(); |
| 2020 | |
| 2021 | if (m_currentState == GST_STATE_READY) |
| 2022 | m_readyState = MediaPlayer::HaveNothing; |
| 2023 | else if (m_currentState == GST_STATE_PAUSED) { |
| 2024 | m_readyState = MediaPlayer::HaveEnoughData; |
| 2025 | m_paused = true; |
| 2026 | } else if (m_currentState == GST_STATE_PLAYING) |
| 2027 | m_paused = false; |
| 2028 | |
| 2029 | if (!m_paused && m_playbackRate) |
| 2030 | changePipelineState(GST_STATE_PLAYING); |
| 2031 | |
| 2032 | m_networkState = MediaPlayer::Loading; |
| 2033 | break; |
| 2034 | default: |
| 2035 | GST_DEBUG_OBJECT(pipeline(), "Else : %d" , getStateResult); |
| 2036 | break; |
| 2037 | } |
| 2038 | |
| 2039 | m_requestedState = GST_STATE_VOID_PENDING; |
| 2040 | |
| 2041 | if (shouldUpdatePlaybackState) |
| 2042 | m_player->playbackStateChanged(); |
| 2043 | |
| 2044 | if (m_networkState != oldNetworkState) { |
| 2045 | GST_DEBUG_OBJECT(pipeline(), "Network State Changed from %s to %s" , convertEnumerationToString(oldNetworkState).utf8().data(), convertEnumerationToString(m_networkState).utf8().data()); |
| 2046 | m_player->networkStateChanged(); |
| 2047 | } |
| 2048 | if (m_readyState != oldReadyState) { |
| 2049 | GST_DEBUG_OBJECT(pipeline(), "Ready State Changed from %s to %s" , convertEnumerationToString(oldReadyState).utf8().data(), convertEnumerationToString(m_readyState).utf8().data()); |
| 2050 | m_player->readyStateChanged(); |
| 2051 | } |
| 2052 | |
| 2053 | if (getStateResult == GST_STATE_CHANGE_SUCCESS && m_currentState >= GST_STATE_PAUSED) { |
| 2054 | updatePlaybackRate(); |
| 2055 | if (m_seekIsPending) { |
| 2056 | GST_DEBUG_OBJECT(pipeline(), "[Seek] committing pending seek to %s" , toString(m_seekTime).utf8().data()); |
| 2057 | m_seekIsPending = false; |
| 2058 | m_seeking = doSeek(m_seekTime, m_player->rate(), static_cast<GstSeekFlags>(GST_SEEK_FLAG_FLUSH | GST_SEEK_FLAG_ACCURATE)); |
| 2059 | if (!m_seeking) { |
| 2060 | m_cachedPosition = MediaTime::invalidTime(); |
| 2061 | GST_DEBUG_OBJECT(pipeline(), "[Seek] seeking to %s failed" , toString(m_seekTime).utf8().data()); |
| 2062 | } |
| 2063 | } |
| 2064 | } |
| 2065 | } |
| 2066 | |
| 2067 | bool MediaPlayerPrivateGStreamer::handleSyncMessage(GstMessage* message) |
| 2068 | { |
| 2069 | #if GST_CHECK_VERSION(1, 10, 0) |
| 2070 | if (GST_MESSAGE_TYPE(message) == GST_MESSAGE_STREAM_COLLECTION && !m_isLegacyPlaybin) { |
| 2071 | GRefPtr<GstStreamCollection> collection; |
| 2072 | gst_message_parse_stream_collection(message, &collection.outPtr()); |
| 2073 | |
| 2074 | if (collection) { |
| 2075 | m_streamCollection.swap(collection); |
| 2076 | m_notifier->notify(MainThreadNotification::StreamCollectionChanged, [this] { |
| 2077 | this->updateTracks(); |
| 2078 | }); |
| 2079 | } |
| 2080 | } |
| 2081 | #endif |
| 2082 | |
| 2083 | return MediaPlayerPrivateGStreamerBase::handleSyncMessage(message); |
| 2084 | } |
| 2085 | |
| 2086 | void MediaPlayerPrivateGStreamer::mediaLocationChanged(GstMessage* message) |
| 2087 | { |
| 2088 | if (m_mediaLocations) |
| 2089 | gst_structure_free(m_mediaLocations); |
| 2090 | |
| 2091 | const GstStructure* structure = gst_message_get_structure(message); |
| 2092 | if (structure) { |
| 2093 | // This structure can contain: |
| 2094 | // - both a new-location string and embedded locations structure |
| 2095 | // - or only a new-location string. |
| 2096 | m_mediaLocations = gst_structure_copy(structure); |
| 2097 | const GValue* locations = gst_structure_get_value(m_mediaLocations, "locations" ); |
| 2098 | |
| 2099 | if (locations) |
| 2100 | m_mediaLocationCurrentIndex = static_cast<int>(gst_value_list_get_size(locations)) -1; |
| 2101 | |
| 2102 | loadNextLocation(); |
| 2103 | } |
| 2104 | } |
| 2105 | |
| 2106 | bool MediaPlayerPrivateGStreamer::loadNextLocation() |
| 2107 | { |
| 2108 | if (!m_mediaLocations) |
| 2109 | return false; |
| 2110 | |
| 2111 | const GValue* locations = gst_structure_get_value(m_mediaLocations, "locations" ); |
| 2112 | const gchar* newLocation = nullptr; |
| 2113 | |
| 2114 | if (!locations) { |
| 2115 | // Fallback on new-location string. |
| 2116 | newLocation = gst_structure_get_string(m_mediaLocations, "new-location" ); |
| 2117 | if (!newLocation) |
| 2118 | return false; |
| 2119 | } |
| 2120 | |
| 2121 | if (!newLocation) { |
| 2122 | if (m_mediaLocationCurrentIndex < 0) { |
| 2123 | m_mediaLocations = nullptr; |
| 2124 | return false; |
| 2125 | } |
| 2126 | |
| 2127 | const GValue* location = gst_value_list_get_value(locations, m_mediaLocationCurrentIndex); |
| 2128 | const GstStructure* structure = gst_value_get_structure(location); |
| 2129 | |
| 2130 | if (!structure) { |
| 2131 | m_mediaLocationCurrentIndex--; |
| 2132 | return false; |
| 2133 | } |
| 2134 | |
| 2135 | newLocation = gst_structure_get_string(structure, "new-location" ); |
| 2136 | } |
| 2137 | |
| 2138 | if (newLocation) { |
| 2139 | // Found a candidate. new-location is not always an absolute url |
| 2140 | // though. We need to take the base of the current url and |
| 2141 | // append the value of new-location to it. |
| 2142 | URL baseUrl = gst_uri_is_valid(newLocation) ? URL() : m_url; |
| 2143 | URL newUrl = URL(baseUrl, newLocation); |
| 2144 | |
| 2145 | auto securityOrigin = SecurityOrigin::create(m_url); |
| 2146 | if (securityOrigin->canRequest(newUrl)) { |
| 2147 | GST_INFO_OBJECT(pipeline(), "New media url: %s" , newUrl.string().utf8().data()); |
| 2148 | |
| 2149 | // Reset player states. |
| 2150 | m_networkState = MediaPlayer::Loading; |
| 2151 | m_player->networkStateChanged(); |
| 2152 | m_readyState = MediaPlayer::HaveNothing; |
| 2153 | m_player->readyStateChanged(); |
| 2154 | |
| 2155 | // Reset pipeline state. |
| 2156 | m_resetPipeline = true; |
| 2157 | changePipelineState(GST_STATE_READY); |
| 2158 | |
| 2159 | GstState state; |
| 2160 | gst_element_get_state(m_pipeline.get(), &state, nullptr, 0); |
| 2161 | if (state <= GST_STATE_READY) { |
| 2162 | // Set the new uri and start playing. |
| 2163 | setPlaybinURL(newUrl); |
| 2164 | changePipelineState(GST_STATE_PLAYING); |
| 2165 | return true; |
| 2166 | } |
| 2167 | } else |
| 2168 | GST_INFO_OBJECT(pipeline(), "Not allowed to load new media location: %s" , newUrl.string().utf8().data()); |
| 2169 | } |
| 2170 | m_mediaLocationCurrentIndex--; |
| 2171 | return false; |
| 2172 | } |
| 2173 | |
| 2174 | void MediaPlayerPrivateGStreamer::loadStateChanged() |
| 2175 | { |
| 2176 | updateStates(); |
| 2177 | } |
| 2178 | |
| 2179 | void MediaPlayerPrivateGStreamer::timeChanged() |
| 2180 | { |
| 2181 | updateStates(); |
| 2182 | m_player->timeChanged(); |
| 2183 | } |
| 2184 | |
| 2185 | void MediaPlayerPrivateGStreamer::didEnd() |
| 2186 | { |
| 2187 | GST_INFO_OBJECT(pipeline(), "Playback ended" ); |
| 2188 | |
| 2189 | // Synchronize position and duration values to not confuse the |
| 2190 | // HTMLMediaElement. In some cases like reverse playback the |
| 2191 | // position is not always reported as 0 for instance. |
| 2192 | m_cachedPosition = MediaTime::invalidTime(); |
| 2193 | MediaTime now = currentMediaTime(); |
| 2194 | if (now > MediaTime::zeroTime() && !m_seeking) { |
| 2195 | m_cachedDuration = now; |
| 2196 | m_player->durationChanged(); |
| 2197 | } |
| 2198 | |
| 2199 | m_isEndReached = true; |
| 2200 | |
| 2201 | if (!m_player->client().mediaPlayerIsLooping()) { |
| 2202 | m_paused = true; |
| 2203 | changePipelineState(GST_STATE_READY); |
| 2204 | m_downloadFinished = false; |
| 2205 | } |
| 2206 | timeChanged(); |
| 2207 | } |
| 2208 | |
| 2209 | void MediaPlayerPrivateGStreamer::durationChanged() |
| 2210 | { |
| 2211 | MediaTime previousDuration = durationMediaTime(); |
| 2212 | m_cachedDuration = MediaTime::invalidTime(); |
| 2213 | |
| 2214 | // Avoid emiting durationchanged in the case where the previous |
| 2215 | // duration was 0 because that case is already handled by the |
| 2216 | // HTMLMediaElement. |
| 2217 | if (previousDuration && durationMediaTime() != previousDuration) |
| 2218 | m_player->durationChanged(); |
| 2219 | } |
| 2220 | |
| 2221 | void MediaPlayerPrivateGStreamer::loadingFailed(MediaPlayer::NetworkState networkError, MediaPlayer::ReadyState readyState, bool forceNotifications) |
| 2222 | { |
| 2223 | GST_WARNING("Loading failed, error: %s" , convertEnumerationToString(networkError).utf8().data()); |
| 2224 | |
| 2225 | m_errorOccured = true; |
| 2226 | if (forceNotifications || m_networkState != networkError) { |
| 2227 | m_networkState = networkError; |
| 2228 | m_player->networkStateChanged(); |
| 2229 | } |
| 2230 | if (forceNotifications || m_readyState != readyState) { |
| 2231 | m_readyState = readyState; |
| 2232 | m_player->readyStateChanged(); |
| 2233 | } |
| 2234 | |
| 2235 | // Loading failed, remove ready timer. |
| 2236 | m_readyTimerHandler.stop(); |
| 2237 | } |
| 2238 | |
| 2239 | void MediaPlayerPrivateGStreamer::getSupportedTypes(HashSet<String, ASCIICaseInsensitiveHash>& types) |
| 2240 | { |
| 2241 | auto& gstRegistryScanner = GStreamerRegistryScanner::singleton(); |
| 2242 | types = gstRegistryScanner.mimeTypeSet(); |
| 2243 | } |
| 2244 | |
| 2245 | MediaPlayer::SupportsType MediaPlayerPrivateGStreamer::supportsType(const MediaEngineSupportParameters& parameters) |
| 2246 | { |
| 2247 | MediaPlayer::SupportsType result = MediaPlayer::IsNotSupported; |
| 2248 | #if ENABLE(MEDIA_SOURCE) |
| 2249 | // MediaPlayerPrivateGStreamerMSE is in charge of mediasource playback, not us. |
| 2250 | if (parameters.isMediaSource) |
| 2251 | return result; |
| 2252 | #endif |
| 2253 | |
| 2254 | #if !ENABLE(MEDIA_STREAM) || !GST_CHECK_VERSION(1, 10, 0) |
| 2255 | if (parameters.isMediaStream) |
| 2256 | return result; |
| 2257 | #endif |
| 2258 | |
| 2259 | if (parameters.type.isEmpty()) |
| 2260 | return result; |
| 2261 | |
| 2262 | GST_DEBUG("Checking mime-type \"%s\"" , parameters.type.raw().utf8().data()); |
| 2263 | auto containerType = parameters.type.containerType(); |
| 2264 | auto& gstRegistryScanner = GStreamerRegistryScanner::singleton(); |
| 2265 | if (gstRegistryScanner.isContainerTypeSupported(containerType)) { |
| 2266 | // Spec says we should not return "probably" if the codecs string is empty. |
| 2267 | Vector<String> codecs = parameters.type.codecs(); |
| 2268 | result = codecs.isEmpty() ? MediaPlayer::MayBeSupported : (gstRegistryScanner.areAllCodecsSupported(codecs) ? MediaPlayer::IsSupported : MediaPlayer::IsNotSupported); |
| 2269 | } |
| 2270 | |
| 2271 | auto finalResult = extendedSupportsType(parameters, result); |
| 2272 | GST_DEBUG("Supported: %s" , convertEnumerationToString(finalResult).utf8().data()); |
| 2273 | return finalResult; |
| 2274 | } |
| 2275 | |
| 2276 | void MediaPlayerPrivateGStreamer::setDownloadBuffering() |
| 2277 | { |
| 2278 | if (!m_pipeline) |
| 2279 | return; |
| 2280 | |
| 2281 | unsigned flags; |
| 2282 | g_object_get(m_pipeline.get(), "flags" , &flags, nullptr); |
| 2283 | |
| 2284 | unsigned flagDownload = getGstPlayFlag("download" ); |
| 2285 | |
| 2286 | // We don't want to stop downloading if we already started it. |
| 2287 | if (flags & flagDownload && m_readyState > MediaPlayer::HaveNothing && !m_resetPipeline) { |
| 2288 | GST_DEBUG_OBJECT(pipeline(), "Download already started, not starting again" ); |
| 2289 | return; |
| 2290 | } |
| 2291 | |
| 2292 | bool shouldDownload = !isLiveStream() && m_preload == MediaPlayer::Auto; |
| 2293 | if (shouldDownload) { |
| 2294 | GST_INFO_OBJECT(pipeline(), "Enabling on-disk buffering" ); |
| 2295 | g_object_set(m_pipeline.get(), "flags" , flags | flagDownload, nullptr); |
| 2296 | m_fillTimer.startRepeating(200_ms); |
| 2297 | } else { |
| 2298 | GST_INFO_OBJECT(pipeline(), "Disabling on-disk buffering" ); |
| 2299 | g_object_set(m_pipeline.get(), "flags" , flags & ~flagDownload, nullptr); |
| 2300 | m_fillTimer.stop(); |
| 2301 | } |
| 2302 | } |
| 2303 | |
| 2304 | void MediaPlayerPrivateGStreamer::setPreload(MediaPlayer::Preload preload) |
| 2305 | { |
| 2306 | GST_DEBUG_OBJECT(pipeline(), "Setting preload to %s" , convertEnumerationToString(preload).utf8().data()); |
| 2307 | if (preload == MediaPlayer::Auto && isLiveStream()) |
| 2308 | return; |
| 2309 | |
| 2310 | m_preload = preload; |
| 2311 | setDownloadBuffering(); |
| 2312 | |
| 2313 | if (m_delayingLoad && m_preload != MediaPlayer::None) { |
| 2314 | m_delayingLoad = false; |
| 2315 | commitLoad(); |
| 2316 | } |
| 2317 | } |
| 2318 | |
| 2319 | GstElement* MediaPlayerPrivateGStreamer::createAudioSink() |
| 2320 | { |
| 2321 | m_autoAudioSink = gst_element_factory_make("autoaudiosink" , nullptr); |
| 2322 | if (!m_autoAudioSink) { |
| 2323 | GST_WARNING("GStreamer's autoaudiosink not found. Please check your gst-plugins-good installation" ); |
| 2324 | return nullptr; |
| 2325 | } |
| 2326 | |
| 2327 | g_signal_connect_swapped(m_autoAudioSink.get(), "child-added" , G_CALLBACK(setAudioStreamPropertiesCallback), this); |
| 2328 | |
| 2329 | #if ENABLE(WEB_AUDIO) |
| 2330 | GstElement* audioSinkBin = gst_bin_new("audio-sink" ); |
| 2331 | ensureAudioSourceProvider(); |
| 2332 | m_audioSourceProvider->configureAudioBin(audioSinkBin, nullptr); |
| 2333 | return audioSinkBin; |
| 2334 | #else |
| 2335 | return m_autoAudioSink.get(); |
| 2336 | #endif |
| 2337 | } |
| 2338 | |
| 2339 | GstElement* MediaPlayerPrivateGStreamer::audioSink() const |
| 2340 | { |
| 2341 | GstElement* sink; |
| 2342 | g_object_get(m_pipeline.get(), "audio-sink" , &sink, nullptr); |
| 2343 | return sink; |
| 2344 | } |
| 2345 | |
| 2346 | #if ENABLE(WEB_AUDIO) |
| 2347 | void MediaPlayerPrivateGStreamer::ensureAudioSourceProvider() |
| 2348 | { |
| 2349 | if (!m_audioSourceProvider) |
| 2350 | m_audioSourceProvider = std::make_unique<AudioSourceProviderGStreamer>(); |
| 2351 | } |
| 2352 | |
| 2353 | AudioSourceProvider* MediaPlayerPrivateGStreamer::audioSourceProvider() |
| 2354 | { |
| 2355 | ensureAudioSourceProvider(); |
| 2356 | return m_audioSourceProvider.get(); |
| 2357 | } |
| 2358 | #endif |
| 2359 | |
| 2360 | void MediaPlayerPrivateGStreamer::createGSTPlayBin(const URL& url, const String& pipelineName) |
| 2361 | { |
| 2362 | const gchar* playbinName = "playbin" ; |
| 2363 | |
| 2364 | // MSE doesn't support playbin3. Mediastream requires playbin3. Regular |
| 2365 | // playback can use playbin3 on-demand with the WEBKIT_GST_USE_PLAYBIN3 |
| 2366 | // environment variable. |
| 2367 | #if GST_CHECK_VERSION(1, 10, 0) |
| 2368 | if ((!isMediaSource() && g_getenv("WEBKIT_GST_USE_PLAYBIN3" )) || url.protocolIs("mediastream" )) |
| 2369 | playbinName = "playbin3" ; |
| 2370 | #endif |
| 2371 | |
| 2372 | if (m_pipeline) { |
| 2373 | if (!g_strcmp0(GST_OBJECT_NAME(gst_element_get_factory(m_pipeline.get())), playbinName)) { |
| 2374 | GST_INFO_OBJECT(pipeline(), "Already using %s" , playbinName); |
| 2375 | return; |
| 2376 | } |
| 2377 | |
| 2378 | GST_INFO_OBJECT(pipeline(), "Tearing down as we need to use %s now." , playbinName); |
| 2379 | changePipelineState(GST_STATE_NULL); |
| 2380 | m_pipeline = nullptr; |
| 2381 | } |
| 2382 | |
| 2383 | ASSERT(!m_pipeline); |
| 2384 | |
| 2385 | m_isLegacyPlaybin = !g_strcmp0(playbinName, "playbin" ); |
| 2386 | |
| 2387 | // gst_element_factory_make() returns a floating reference so |
| 2388 | // we should not adopt. |
| 2389 | setPipeline(gst_element_factory_make(playbinName, |
| 2390 | (pipelineName.isEmpty() ? makeString("play_0x" , hex(reinterpret_cast<uintptr_t>(this), Lowercase)) : pipelineName).utf8().data())); |
| 2391 | setStreamVolumeElement(GST_STREAM_VOLUME(m_pipeline.get())); |
| 2392 | |
| 2393 | GST_INFO_OBJECT(pipeline(), "Using legacy playbin element: %s" , boolForPrinting(m_isLegacyPlaybin)); |
| 2394 | |
| 2395 | // Let also other listeners subscribe to (application) messages in this bus. |
| 2396 | GRefPtr<GstBus> bus = adoptGRef(gst_pipeline_get_bus(GST_PIPELINE(m_pipeline.get()))); |
| 2397 | gst_bus_add_signal_watch_full(bus.get(), RunLoopSourcePriority::RunLoopDispatcher); |
| 2398 | g_signal_connect(bus.get(), "message" , G_CALLBACK(busMessageCallback), this); |
| 2399 | |
| 2400 | g_object_set(m_pipeline.get(), "mute" , m_player->muted(), nullptr); |
| 2401 | |
| 2402 | g_signal_connect(GST_BIN_CAST(m_pipeline.get()), "deep-element-added" , G_CALLBACK(+[](GstBin*, GstBin* subBin, GstElement* element, MediaPlayerPrivateGStreamer* player) { |
| 2403 | GUniquePtr<char> binName(gst_element_get_name(GST_ELEMENT_CAST(subBin))); |
| 2404 | if (!g_str_has_prefix(binName.get(), "decodebin" )) |
| 2405 | return; |
| 2406 | |
| 2407 | GUniquePtr<char> elementName(gst_element_get_name(element)); |
| 2408 | if (g_str_has_prefix(elementName.get(), "v4l2" )) |
| 2409 | player->m_videoDecoderPlatform = WebKitGstVideoDecoderPlatform::Video4Linux; |
| 2410 | else if (g_str_has_prefix(elementName.get(), "imxvpudecoder" )) |
| 2411 | player->m_videoDecoderPlatform = WebKitGstVideoDecoderPlatform::ImxVPU; |
| 2412 | |
| 2413 | player->updateTextureMapperFlags(); |
| 2414 | }), this); |
| 2415 | |
| 2416 | g_signal_connect_swapped(m_pipeline.get(), "source-setup" , G_CALLBACK(sourceSetupCallback), this); |
| 2417 | if (m_isLegacyPlaybin) { |
| 2418 | g_signal_connect_swapped(m_pipeline.get(), "video-changed" , G_CALLBACK(videoChangedCallback), this); |
| 2419 | g_signal_connect_swapped(m_pipeline.get(), "audio-changed" , G_CALLBACK(audioChangedCallback), this); |
| 2420 | } |
| 2421 | |
| 2422 | #if ENABLE(VIDEO_TRACK) |
| 2423 | if (m_isLegacyPlaybin) |
| 2424 | g_signal_connect_swapped(m_pipeline.get(), "text-changed" , G_CALLBACK(textChangedCallback), this); |
| 2425 | |
| 2426 | GstElement* textCombiner = webkitTextCombinerNew(); |
| 2427 | ASSERT(textCombiner); |
| 2428 | g_object_set(m_pipeline.get(), "text-stream-combiner" , textCombiner, nullptr); |
| 2429 | |
| 2430 | m_textAppSink = webkitTextSinkNew(); |
| 2431 | ASSERT(m_textAppSink); |
| 2432 | |
| 2433 | m_textAppSinkPad = adoptGRef(gst_element_get_static_pad(m_textAppSink.get(), "sink" )); |
| 2434 | ASSERT(m_textAppSinkPad); |
| 2435 | |
| 2436 | GRefPtr<GstCaps> textCaps; |
| 2437 | if (webkitGstCheckVersion(1, 14, 0)) |
| 2438 | textCaps = adoptGRef(gst_caps_new_empty_simple("application/x-subtitle-vtt" )); |
| 2439 | else |
| 2440 | textCaps = adoptGRef(gst_caps_new_empty_simple("text/vtt" )); |
| 2441 | g_object_set(m_textAppSink.get(), "emit-signals" , TRUE, "enable-last-sample" , FALSE, "caps" , textCaps.get(), nullptr); |
| 2442 | g_signal_connect_swapped(m_textAppSink.get(), "new-sample" , G_CALLBACK(newTextSampleCallback), this); |
| 2443 | |
| 2444 | g_object_set(m_pipeline.get(), "text-sink" , m_textAppSink.get(), nullptr); |
| 2445 | #endif |
| 2446 | |
| 2447 | g_object_set(m_pipeline.get(), "video-sink" , createVideoSink(), "audio-sink" , createAudioSink(), nullptr); |
| 2448 | |
| 2449 | configurePlaySink(); |
| 2450 | |
| 2451 | if (m_preservesPitch) { |
| 2452 | GstElement* scale = gst_element_factory_make("scaletempo" , nullptr); |
| 2453 | |
| 2454 | if (!scale) |
| 2455 | GST_WARNING("Failed to create scaletempo" ); |
| 2456 | else |
| 2457 | g_object_set(m_pipeline.get(), "audio-filter" , scale, nullptr); |
| 2458 | } |
| 2459 | |
| 2460 | if (!m_renderingCanBeAccelerated) { |
| 2461 | // If not using accelerated compositing, let GStreamer handle |
| 2462 | // the image-orientation tag. |
| 2463 | GstElement* videoFlip = gst_element_factory_make("videoflip" , nullptr); |
| 2464 | if (videoFlip) { |
| 2465 | g_object_set(videoFlip, "method" , 8, nullptr); |
| 2466 | g_object_set(m_pipeline.get(), "video-filter" , videoFlip, nullptr); |
| 2467 | } else |
| 2468 | GST_WARNING("The videoflip element is missing, video rotation support is now disabled. Please check your gst-plugins-good installation." ); |
| 2469 | } |
| 2470 | |
| 2471 | GRefPtr<GstPad> videoSinkPad = adoptGRef(gst_element_get_static_pad(m_videoSink.get(), "sink" )); |
| 2472 | if (videoSinkPad) |
| 2473 | g_signal_connect_swapped(videoSinkPad.get(), "notify::caps" , G_CALLBACK(videoSinkCapsChangedCallback), this); |
| 2474 | } |
| 2475 | |
| 2476 | void MediaPlayerPrivateGStreamer::simulateAudioInterruption() |
| 2477 | { |
| 2478 | GstMessage* message = gst_message_new_request_state(GST_OBJECT(m_pipeline.get()), GST_STATE_PAUSED); |
| 2479 | gst_element_post_message(m_pipeline.get(), message); |
| 2480 | } |
| 2481 | |
| 2482 | bool MediaPlayerPrivateGStreamer::didPassCORSAccessCheck() const |
| 2483 | { |
| 2484 | if (WEBKIT_IS_WEB_SRC(m_source.get())) |
| 2485 | return webKitSrcPassedCORSAccessCheck(WEBKIT_WEB_SRC(m_source.get())); |
| 2486 | return false; |
| 2487 | } |
| 2488 | |
| 2489 | bool MediaPlayerPrivateGStreamer::canSaveMediaData() const |
| 2490 | { |
| 2491 | if (isLiveStream()) |
| 2492 | return false; |
| 2493 | |
| 2494 | if (m_url.isLocalFile()) |
| 2495 | return true; |
| 2496 | |
| 2497 | if (m_url.protocolIsInHTTPFamily()) |
| 2498 | return true; |
| 2499 | |
| 2500 | return false; |
| 2501 | } |
| 2502 | |
| 2503 | Optional<bool> MediaPlayerPrivateGStreamer::wouldTaintOrigin(const SecurityOrigin& origin) const |
| 2504 | { |
| 2505 | if (webkitGstCheckVersion(1, 12, 0)) { |
| 2506 | GST_TRACE_OBJECT(pipeline(), "Checking %u origins" , m_origins.size()); |
| 2507 | for (auto& responseOrigin : m_origins) { |
| 2508 | if (!origin.canAccess(*responseOrigin)) { |
| 2509 | GST_DEBUG_OBJECT(pipeline(), "Found reachable response origin" ); |
| 2510 | return true; |
| 2511 | } |
| 2512 | } |
| 2513 | GST_DEBUG_OBJECT(pipeline(), "No valid response origin found" ); |
| 2514 | return false; |
| 2515 | } |
| 2516 | |
| 2517 | // GStreamer < 1.12 has an incomplete uridownloader implementation so we |
| 2518 | // can't use WebKitWebSrc for adaptive fragments downloading if this |
| 2519 | // version is detected. |
| 2520 | UNUSED_PARAM(origin); |
| 2521 | return m_hasTaintedOrigin; |
| 2522 | } |
| 2523 | |
| 2524 | } |
| 2525 | |
| 2526 | #endif // USE(GSTREAMER) |
| 2527 | |