summaryrefslogtreecommitdiffstats
path: root/dom/media/mediasource
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/mediasource')
-rw-r--r--dom/media/mediasource/AsyncEventRunner.h36
-rw-r--r--dom/media/mediasource/AutoTaskQueue.h59
-rw-r--r--dom/media/mediasource/ContainerParser.cpp716
-rw-r--r--dom/media/mediasource/ContainerParser.h90
-rw-r--r--dom/media/mediasource/MediaSource.cpp591
-rw-r--r--dom/media/mediasource/MediaSource.h157
-rw-r--r--dom/media/mediasource/MediaSourceDecoder.cpp361
-rw-r--r--dom/media/mediasource/MediaSourceDecoder.h96
-rw-r--r--dom/media/mediasource/MediaSourceDemuxer.cpp499
-rw-r--r--dom/media/mediasource/MediaSourceDemuxer.h145
-rw-r--r--dom/media/mediasource/MediaSourceResource.h109
-rw-r--r--dom/media/mediasource/MediaSourceUtils.cpp34
-rw-r--r--dom/media/mediasource/MediaSourceUtils.h19
-rw-r--r--dom/media/mediasource/ResourceQueue.cpp214
-rw-r--r--dom/media/mediasource/ResourceQueue.h86
-rw-r--r--dom/media/mediasource/SourceBuffer.cpp596
-rw-r--r--dom/media/mediasource/SourceBuffer.h189
-rw-r--r--dom/media/mediasource/SourceBufferAttributes.h157
-rw-r--r--dom/media/mediasource/SourceBufferList.cpp225
-rw-r--r--dom/media/mediasource/SourceBufferList.h106
-rw-r--r--dom/media/mediasource/SourceBufferResource.cpp185
-rw-r--r--dom/media/mediasource/SourceBufferResource.h161
-rw-r--r--dom/media/mediasource/SourceBufferTask.h111
-rw-r--r--dom/media/mediasource/TrackBuffersManager.cpp2505
-rw-r--r--dom/media/mediasource/TrackBuffersManager.h500
-rw-r--r--dom/media/mediasource/gtest/TestContainerParser.cpp92
-rw-r--r--dom/media/mediasource/gtest/moz.build16
-rw-r--r--dom/media/mediasource/moz.build49
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-1.m4sbin0 -> 24328 bytes
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-2.m4sbin0 -> 24132 bytes
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-init.mp4bin0 -> 1246 bytes
-rw-r--r--dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-1.m4sbin0 -> 48979 bytes
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-2.m4sbin0 -> 47727 bytes
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-init.mp4bin0 -> 634 bytes
-rw-r--r--dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop1.m4sbin0 -> 24424 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop10.m4sbin0 -> 18279 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop11.m4sbin0 -> 24607 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop12.m4sbin0 -> 22676 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop13.m4sbin0 -> 9847 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2.m4sbin0 -> 22205 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2s.mp4bin0 -> 48024 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop3.m4sbin0 -> 24013 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop4.m4sbin0 -> 23112 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop5.m4sbin0 -> 18367 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop6.m4sbin0 -> 24455 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop7.m4sbin0 -> 22442 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop8.m4sbin0 -> 24356 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop9.m4sbin0 -> 23252 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^1
-rwxr-xr-xdom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4sbin0 -> 66806 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^1
-rwxr-xr-xdom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4sbin0 -> 65292 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^1
-rwxr-xr-xdom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4bin0 -> 1410 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio1.m4sbin0 -> 694 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio10.m4sbin0 -> 879 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio11.m4sbin0 -> 208 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio2.m4sbin0 -> 750 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio3.m4sbin0 -> 724 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio4.m4sbin0 -> 806 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio5.m4sbin0 -> 822 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio6.m4sbin0 -> 833 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio7.m4sbin0 -> 888 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio8.m4sbin0 -> 829 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio9.m4sbin0 -> 778 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4bin0 -> 825 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_dash.mpd48
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video1.m4sbin0 -> 23860 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video10.m4sbin0 -> 18109 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video11.m4sbin0 -> 23969 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video12.m4sbin0 -> 21937 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video13.m4sbin0 -> 16265 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video2.m4sbin0 -> 21595 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video3.m4sbin0 -> 23429 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video4.m4sbin0 -> 22446 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video5.m4sbin0 -> 18191 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video6.m4sbin0 -> 23773 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video7.m4sbin0 -> 21749 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video8.m4sbin0 -> 23608 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video9.m4sbin0 -> 22553 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4bin0 -> 887 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/bipbop/bipbopinit.mp4bin0 -> 1395 bytes
-rw-r--r--dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^1
-rw-r--r--dom/media/mediasource/test/crashtests/1005366.html27
-rw-r--r--dom/media/mediasource/test/crashtests/1059035.html26
-rw-r--r--dom/media/mediasource/test/crashtests/926665.html26
-rw-r--r--dom/media/mediasource/test/crashtests/931388.html17
-rw-r--r--dom/media/mediasource/test/crashtests/crashtests.list4
-rw-r--r--dom/media/mediasource/test/mediasource.js130
-rw-r--r--dom/media/mediasource/test/mochitest.ini137
-rw-r--r--dom/media/mediasource/test/seek.webmbin0 -> 215529 bytes
-rw-r--r--dom/media/mediasource/test/seek.webm^headers^1
-rw-r--r--dom/media/mediasource/test/seek_lowres.webmbin0 -> 100749 bytes
-rw-r--r--dom/media/mediasource/test/seek_lowres.webm^headers^1
-rw-r--r--dom/media/mediasource/test/test_AudioChange_mp4.html72
-rw-r--r--dom/media/mediasource/test/test_AutoRevocation.html40
-rw-r--r--dom/media/mediasource/test/test_BufferedSeek.html63
-rw-r--r--dom/media/mediasource/test/test_BufferedSeek_mp4.html63
-rw-r--r--dom/media/mediasource/test/test_BufferingWait.html56
-rw-r--r--dom/media/mediasource/test/test_BufferingWait_mp4.html54
-rw-r--r--dom/media/mediasource/test/test_DrainOnMissingData_mp4.html60
-rw-r--r--dom/media/mediasource/test/test_DurationChange.html111
-rw-r--r--dom/media/mediasource/test/test_DurationUpdated.html57
-rw-r--r--dom/media/mediasource/test/test_DurationUpdated_mp4.html56
-rw-r--r--dom/media/mediasource/test/test_EndOfStream.html50
-rw-r--r--dom/media/mediasource/test/test_EndOfStream_mp4.html50
-rw-r--r--dom/media/mediasource/test/test_Eviction_mp4.html82
-rw-r--r--dom/media/mediasource/test/test_FrameSelection.html78
-rw-r--r--dom/media/mediasource/test/test_FrameSelection_mp4.html73
-rw-r--r--dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html47
-rw-r--r--dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html52
-rw-r--r--dom/media/mediasource/test/test_LiveSeekable.html80
-rw-r--r--dom/media/mediasource/test/test_LoadedDataFired_mp4.html68
-rw-r--r--dom/media/mediasource/test/test_LoadedMetadataFired.html37
-rw-r--r--dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html37
-rw-r--r--dom/media/mediasource/test/test_MediaSource.html107
-rw-r--r--dom/media/mediasource/test/test_MediaSource_disabled.html32
-rw-r--r--dom/media/mediasource/test/test_MediaSource_memory_reporting.html51
-rw-r--r--dom/media/mediasource/test/test_MediaSource_mp4.html108
-rw-r--r--dom/media/mediasource/test/test_MultipleInitSegments.html53
-rw-r--r--dom/media/mediasource/test/test_MultipleInitSegments_mp4.html52
-rw-r--r--dom/media/mediasource/test/test_OnEvents.html65
-rw-r--r--dom/media/mediasource/test/test_PlayEvents.html165
-rw-r--r--dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html55
-rw-r--r--dom/media/mediasource/test/test_SeekNoData_mp4.html69
-rw-r--r--dom/media/mediasource/test/test_SeekToEnd_mp4.html57
-rw-r--r--dom/media/mediasource/test/test_SeekTwice_mp4.html54
-rw-r--r--dom/media/mediasource/test/test_SeekableAfterEndOfStream.html49
-rw-r--r--dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit.html50
-rw-r--r--dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit_mp4.html50
-rw-r--r--dom/media/mediasource/test/test_SeekableAfterEndOfStream_mp4.html49
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeEndOfStream.html38
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit.html42
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit_mp4.html44
-rw-r--r--dom/media/mediasource/test/test_SeekableBeforeEndOfStream_mp4.html38
-rw-r--r--dom/media/mediasource/test/test_SeekedEvent_mp4.html75
-rw-r--r--dom/media/mediasource/test/test_Sequence_mp4.html39
-rw-r--r--dom/media/mediasource/test/test_SetModeThrows.html34
-rw-r--r--dom/media/mediasource/test/test_SplitAppend.html47
-rw-r--r--dom/media/mediasource/test/test_SplitAppendDelay.html50
-rw-r--r--dom/media/mediasource/test/test_SplitAppendDelay_mp4.html51
-rw-r--r--dom/media/mediasource/test/test_SplitAppend_mp4.html48
-rw-r--r--dom/media/mediasource/test/test_Threshold_mp4.html84
-rw-r--r--dom/media/mediasource/test/test_TimestampOffset_mp4.html86
-rw-r--r--dom/media/mediasource/test/test_TruncatedDuration.html78
-rw-r--r--dom/media/mediasource/test/test_TruncatedDuration_mp4.html83
-rw-r--r--dom/media/mediasource/test/test_WaitingOnMissingData.html63
-rw-r--r--dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html53
-rw-r--r--dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html65
-rw-r--r--dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html56
196 files changed, 11997 insertions, 0 deletions
diff --git a/dom/media/mediasource/AsyncEventRunner.h b/dom/media/mediasource/AsyncEventRunner.h
new file mode 100644
index 000000000..a1b6265f7
--- /dev/null
+++ b/dom/media/mediasource/AsyncEventRunner.h
@@ -0,0 +1,36 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_ASYNCEVENTRUNNER_H_
+#define MOZILLA_ASYNCEVENTRUNNER_H_
+
+#include "nsThreadUtils.h"
+
+namespace mozilla {
+
+template <typename T>
+class AsyncEventRunner : public Runnable
+{
+public:
+ AsyncEventRunner(T* aTarget, const char* aName)
+ : mTarget(aTarget)
+ , mName(aName)
+ {}
+
+ NS_IMETHOD Run() override
+ {
+ mTarget->DispatchSimpleEvent(mName);
+ return NS_OK;
+ }
+
+private:
+ RefPtr<T> mTarget;
+ const char* mName;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_ASYNCEVENTRUNNER_H_ */
diff --git a/dom/media/mediasource/AutoTaskQueue.h b/dom/media/mediasource/AutoTaskQueue.h
new file mode 100644
index 000000000..1726ad5be
--- /dev/null
+++ b/dom/media/mediasource/AutoTaskQueue.h
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_AUTOTASKQUEUE_H_
+#define MOZILLA_AUTOTASKQUEUE_H_
+
+#include "mozilla/RefPtr.h"
+#include "mozilla/SharedThreadPool.h"
+#include "mozilla/TaskQueue.h"
+
+namespace mozilla {
+
+// A convenience TaskQueue not requiring explicit shutdown.
+class AutoTaskQueue : public AbstractThread
+{
+public:
+ explicit AutoTaskQueue(already_AddRefed<SharedThreadPool> aPool, bool aSupportsTailDispatch = false)
+ : AbstractThread(aSupportsTailDispatch)
+ , mTaskQueue(new TaskQueue(Move(aPool), aSupportsTailDispatch))
+ {}
+
+ TaskDispatcher& TailDispatcher() override
+ {
+ return mTaskQueue->TailDispatcher();
+ }
+
+ void Dispatch(already_AddRefed<nsIRunnable> aRunnable,
+ DispatchFailureHandling aFailureHandling = AssertDispatchSuccess,
+ DispatchReason aReason = NormalDispatch) override
+ {
+ mTaskQueue->Dispatch(Move(aRunnable), aFailureHandling, aReason);
+ }
+
+ // Blocks until all tasks finish executing.
+ void AwaitIdle() { mTaskQueue->AwaitIdle(); }
+
+ bool IsEmpty() { return mTaskQueue->IsEmpty(); }
+
+ // Returns true if the current thread is currently running a Runnable in
+ // the task queue.
+ bool IsCurrentThreadIn() override { return mTaskQueue->IsCurrentThreadIn(); }
+
+private:
+ ~AutoTaskQueue()
+ {
+ RefPtr<TaskQueue> taskqueue = mTaskQueue;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction([taskqueue]() { taskqueue->BeginShutdown(); });
+ AbstractThread::MainThread()->Dispatch(task.forget());
+ }
+ RefPtr<TaskQueue> mTaskQueue;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mediasource/ContainerParser.cpp b/dom/media/mediasource/ContainerParser.cpp
new file mode 100644
index 000000000..5267e9315
--- /dev/null
+++ b/dom/media/mediasource/ContainerParser.cpp
@@ -0,0 +1,716 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContainerParser.h"
+
+#include "WebMBufferedParser.h"
+#include "mozilla/EndianUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mp4_demuxer/MoofParser.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Maybe.h"
+#include "MediaData.h"
+#ifdef MOZ_FMP4
+#include "MP4Stream.h"
+#include "mp4_demuxer/AtomType.h"
+#include "mp4_demuxer/ByteReader.h"
+#endif
+#include "nsAutoPtr.h"
+#include "SourceBufferResource.h"
+#include <algorithm>
+
+extern mozilla::LogModule* GetMediaSourceSamplesLog();
+
+#define STRINGIFY(x) #x
+#define TOSTRING(x) STRINGIFY(x)
+#define MSE_DEBUG(name, arg, ...) MOZ_LOG(GetMediaSourceSamplesLog(), mozilla::LogLevel::Debug, (TOSTRING(name) "(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+#define MSE_DEBUGV(name, arg, ...) MOZ_LOG(GetMediaSourceSamplesLog(), mozilla::LogLevel::Verbose, (TOSTRING(name) "(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+ContainerParser::ContainerParser(const nsACString& aType)
+ : mHasInitData(false)
+ , mType(aType)
+{
+}
+
+ContainerParser::~ContainerParser() = default;
+
+MediaResult
+ContainerParser::IsInitSegmentPresent(MediaByteBuffer* aData)
+{
+ MSE_DEBUG(ContainerParser, "aLength=%u [%x%x%x%x]",
+ aData->Length(),
+ aData->Length() > 0 ? (*aData)[0] : 0,
+ aData->Length() > 1 ? (*aData)[1] : 0,
+ aData->Length() > 2 ? (*aData)[2] : 0,
+ aData->Length() > 3 ? (*aData)[3] : 0);
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+MediaResult
+ContainerParser::IsMediaSegmentPresent(MediaByteBuffer* aData)
+{
+ MSE_DEBUG(ContainerParser, "aLength=%u [%x%x%x%x]",
+ aData->Length(),
+ aData->Length() > 0 ? (*aData)[0] : 0,
+ aData->Length() > 1 ? (*aData)[1] : 0,
+ aData->Length() > 2 ? (*aData)[2] : 0,
+ aData->Length() > 3 ? (*aData)[3] : 0);
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+MediaResult
+ContainerParser::ParseStartAndEndTimestamps(MediaByteBuffer* aData,
+ int64_t& aStart, int64_t& aEnd)
+{
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+bool
+ContainerParser::TimestampsFuzzyEqual(int64_t aLhs, int64_t aRhs)
+{
+ return llabs(aLhs - aRhs) <= GetRoundingError();
+}
+
+int64_t
+ContainerParser::GetRoundingError()
+{
+ NS_WARNING("Using default ContainerParser::GetRoundingError implementation");
+ return 0;
+}
+
+bool
+ContainerParser::HasCompleteInitData()
+{
+ return mHasInitData && !!mInitData->Length();
+}
+
+MediaByteBuffer*
+ContainerParser::InitData()
+{
+ return mInitData;
+}
+
+MediaByteRange
+ContainerParser::InitSegmentRange()
+{
+ return mCompleteInitSegmentRange;
+}
+
+MediaByteRange
+ContainerParser::MediaHeaderRange()
+{
+ return mCompleteMediaHeaderRange;
+}
+
+MediaByteRange
+ContainerParser::MediaSegmentRange()
+{
+ return mCompleteMediaSegmentRange;
+}
+
+class WebMContainerParser : public ContainerParser {
+public:
+ explicit WebMContainerParser(const nsACString& aType)
+ : ContainerParser(aType)
+ , mParser(0)
+ , mOffset(0)
+ {}
+
+ static const unsigned NS_PER_USEC = 1000;
+ static const unsigned USEC_PER_SEC = 1000000;
+
+ MediaResult IsInitSegmentPresent(MediaByteBuffer* aData) override
+ {
+ ContainerParser::IsInitSegmentPresent(aData);
+ // XXX: This is overly primitive, needs to collect data as it's appended
+ // to the SB and handle, rather than assuming everything is present in a
+ // single aData segment.
+ // 0x1a45dfa3 // EBML
+ // ...
+ // DocType == "webm"
+ // ...
+ // 0x18538067 // Segment (must be "unknown" size or contain a value large
+ // enough to include the Segment Information and Tracks
+ // elements that follow)
+ // 0x1549a966 // -> Segment Info
+ // 0x1654ae6b // -> One or more Tracks
+
+ // 0x1a45dfa3 // EBML
+ if (aData->Length() < 4) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if ((*aData)[0] == 0x1a && (*aData)[1] == 0x45 && (*aData)[2] == 0xdf &&
+ (*aData)[3] == 0xa3) {
+ return NS_OK;
+ }
+ return MediaResult(NS_ERROR_FAILURE, RESULT_DETAIL("Invalid webm content"));
+ }
+
+ MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData) override
+ {
+ ContainerParser::IsMediaSegmentPresent(aData);
+ // XXX: This is overly primitive, needs to collect data as it's appended
+ // to the SB and handle, rather than assuming everything is present in a
+ // single aData segment.
+ // 0x1a45dfa3 // EBML
+ // ...
+ // DocType == "webm"
+ // ...
+ // 0x18538067 // Segment (must be "unknown" size)
+ // 0x1549a966 // -> Segment Info
+ // 0x1654ae6b // -> One or more Tracks
+
+ // 0x1f43b675 // Cluster
+ if (aData->Length() < 4) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ if ((*aData)[0] == 0x1f && (*aData)[1] == 0x43 && (*aData)[2] == 0xb6 &&
+ (*aData)[3] == 0x75) {
+ return NS_OK;
+ }
+ // 0x1c53bb6b // Cues
+ if ((*aData)[0] == 0x1c && (*aData)[1] == 0x53 && (*aData)[2] == 0xbb &&
+ (*aData)[3] == 0x6b) {
+ return NS_OK;
+ }
+ return MediaResult(NS_ERROR_FAILURE, RESULT_DETAIL("Invalid webm content"));
+ }
+
+ MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
+ int64_t& aStart,
+ int64_t& aEnd) override
+ {
+ bool initSegment = NS_SUCCEEDED(IsInitSegmentPresent(aData));
+
+ if (mLastMapping &&
+ (initSegment || NS_SUCCEEDED(IsMediaSegmentPresent(aData)))) {
+ // The last data contained a complete cluster but we can only detect it
+ // now that a new one is starting.
+ // We use mOffset as end position to ensure that any blocks not reported
+ // by WebMBufferParser are properly skipped.
+ mCompleteMediaSegmentRange = MediaByteRange(mLastMapping.ref().mSyncOffset,
+ mOffset);
+ mLastMapping.reset();
+ MSE_DEBUG(WebMContainerParser, "New cluster found at start, ending previous one");
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (initSegment) {
+ mOffset = 0;
+ mParser = WebMBufferedParser(0);
+ mOverlappedMapping.Clear();
+ mInitData = new MediaByteBuffer();
+ mResource = new SourceBufferResource(NS_LITERAL_CSTRING("video/webm"));
+ mCompleteMediaHeaderRange = MediaByteRange();
+ mCompleteMediaSegmentRange = MediaByteRange();
+ }
+
+ // XXX if it only adds new mappings, overlapped but not available
+ // (e.g. overlap < 0) frames are "lost" from the reported mappings here.
+ nsTArray<WebMTimeDataOffset> mapping;
+ mapping.AppendElements(mOverlappedMapping);
+ mOverlappedMapping.Clear();
+ ReentrantMonitor dummy("dummy");
+ mParser.Append(aData->Elements(), aData->Length(), mapping, dummy);
+ if (mResource) {
+ mResource->AppendData(aData);
+ }
+
+ // XXX This is a bit of a hack. Assume if there are no timecodes
+ // present and it's an init segment that it's _just_ an init segment.
+ // We should be more precise.
+ if (initSegment || !HasCompleteInitData()) {
+ if (mParser.mInitEndOffset > 0) {
+ MOZ_ASSERT(mParser.mInitEndOffset <= mResource->GetLength());
+ if (!mInitData->SetLength(mParser.mInitEndOffset, fallible)) {
+ // Super unlikely OOM
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ mCompleteInitSegmentRange = MediaByteRange(0, mParser.mInitEndOffset);
+ char* buffer = reinterpret_cast<char*>(mInitData->Elements());
+ mResource->ReadFromCache(buffer, 0, mParser.mInitEndOffset);
+ MSE_DEBUG(WebMContainerParser, "Stashed init of %u bytes.",
+ mParser.mInitEndOffset);
+ mResource = nullptr;
+ } else {
+ MSE_DEBUG(WebMContainerParser, "Incomplete init found.");
+ }
+ mHasInitData = true;
+ }
+ mOffset += aData->Length();
+
+ if (mapping.IsEmpty()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Calculate media range for first media segment.
+
+ // Check if we have a cluster finishing in the current data.
+ uint32_t endIdx = mapping.Length() - 1;
+ bool foundNewCluster = false;
+ while (mapping[0].mSyncOffset != mapping[endIdx].mSyncOffset) {
+ endIdx -= 1;
+ foundNewCluster = true;
+ }
+
+ int32_t completeIdx = endIdx;
+ while (completeIdx >= 0 && mOffset < mapping[completeIdx].mEndOffset) {
+ MSE_DEBUG(WebMContainerParser, "block is incomplete, missing: %lld",
+ mapping[completeIdx].mEndOffset - mOffset);
+ completeIdx -= 1;
+ }
+
+ // Save parsed blocks for which we do not have all data yet.
+ mOverlappedMapping.AppendElements(mapping.Elements() + completeIdx + 1,
+ mapping.Length() - completeIdx - 1);
+
+ if (completeIdx < 0) {
+ mLastMapping.reset();
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (mCompleteMediaHeaderRange.IsEmpty()) {
+ mCompleteMediaHeaderRange = MediaByteRange(mapping[0].mSyncOffset,
+ mapping[0].mEndOffset);
+ }
+
+ if (foundNewCluster && mOffset >= mapping[endIdx].mEndOffset) {
+ // We now have all information required to delimit a complete cluster.
+ int64_t endOffset = mapping[endIdx+1].mSyncOffset;
+ if (mapping[endIdx+1].mInitOffset > mapping[endIdx].mInitOffset) {
+ // We have a new init segment before this cluster.
+ endOffset = mapping[endIdx+1].mInitOffset;
+ }
+ mCompleteMediaSegmentRange = MediaByteRange(mapping[endIdx].mSyncOffset,
+ endOffset);
+ } else if (mapping[endIdx].mClusterEndOffset >= 0 &&
+ mOffset >= mapping[endIdx].mClusterEndOffset) {
+ mCompleteMediaSegmentRange = MediaByteRange(mapping[endIdx].mSyncOffset,
+ mParser.EndSegmentOffset(mapping[endIdx].mClusterEndOffset));
+ }
+
+ Maybe<WebMTimeDataOffset> previousMapping;
+ if (completeIdx) {
+ previousMapping = Some(mapping[completeIdx - 1]);
+ } else {
+ previousMapping = mLastMapping;
+ }
+
+ mLastMapping = Some(mapping[completeIdx]);
+
+ if (!previousMapping && completeIdx + 1u >= mapping.Length()) {
+ // We have no previous nor next block available,
+ // so we can't estimate this block's duration.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ uint64_t frameDuration = (completeIdx + 1u < mapping.Length())
+ ? mapping[completeIdx + 1].mTimecode - mapping[completeIdx].mTimecode
+ : mapping[completeIdx].mTimecode - previousMapping.ref().mTimecode;
+ aStart = mapping[0].mTimecode / NS_PER_USEC;
+ aEnd = (mapping[completeIdx].mTimecode + frameDuration) / NS_PER_USEC;
+
+ MSE_DEBUG(WebMContainerParser, "[%lld, %lld] [fso=%lld, leo=%lld, l=%u processedIdx=%u fs=%lld]",
+ aStart, aEnd, mapping[0].mSyncOffset,
+ mapping[completeIdx].mEndOffset, mapping.Length(), completeIdx,
+ mCompleteMediaSegmentRange.mEnd);
+
+ return NS_OK;
+ }
+
+ int64_t GetRoundingError() override
+ {
+ int64_t error = mParser.GetTimecodeScale() / NS_PER_USEC;
+ return error * 2;
+ }
+
+private:
+ WebMBufferedParser mParser;
+ nsTArray<WebMTimeDataOffset> mOverlappedMapping;
+ int64_t mOffset;
+ Maybe<WebMTimeDataOffset> mLastMapping;
+};
+
+#ifdef MOZ_FMP4
+class MP4ContainerParser : public ContainerParser {
+public:
+ explicit MP4ContainerParser(const nsACString& aType)
+ : ContainerParser(aType)
+ {}
+
+ MediaResult IsInitSegmentPresent(MediaByteBuffer* aData) override
+ {
+ ContainerParser::IsInitSegmentPresent(aData);
+ // Each MP4 atom has a chunk size and chunk type. The root chunk in an MP4
+ // file is the 'ftyp' atom followed by a file type. We just check for a
+ // vaguely valid 'ftyp' atom.
+ if (aData->Length() < 8) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ AtomParser parser(mType, aData);
+ if (!parser.IsValid()) {
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Invalid Top-Level Box:%s", parser.LastInvalidBox()));
+ }
+ return parser.StartWithInitSegment() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData) override
+ {
+ if (aData->Length() < 8) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ AtomParser parser(mType, aData);
+ if (!parser.IsValid()) {
+ return MediaResult(
+ NS_ERROR_FAILURE,
+ RESULT_DETAIL("Invalid Box:%s", parser.LastInvalidBox()));
+ }
+ return parser.StartWithMediaSegment() ? NS_OK : NS_ERROR_NOT_AVAILABLE;
+ }
+
+private:
+ class AtomParser {
+ public:
+ AtomParser(const nsACString& aType, const MediaByteBuffer* aData)
+ {
+ const nsCString mType(aType); // for logging macro.
+ mp4_demuxer::ByteReader reader(aData);
+ mp4_demuxer::AtomType initAtom("ftyp");
+ mp4_demuxer::AtomType mediaAtom("moof");
+
+ // Valid top-level boxes defined in ISO/IEC 14496-12 (Table 1)
+ static const mp4_demuxer::AtomType validBoxes[] = {
+ "ftyp", "moov", // init segment
+ "pdin", "free", "sidx", // optional prior moov box
+ "styp", "moof", "mdat", // media segment
+ "mfra", "skip", "meta", "meco", "ssix", "prft" // others.
+ "pssh", // optional with encrypted EME, though ignored.
+ "emsg", // ISO23009-1:2014 Section 5.10.3.3
+ "bloc", "uuid" // boxes accepted by chrome.
+ };
+
+ while (reader.Remaining() >= 8) {
+ uint64_t size = reader.ReadU32();
+ const uint8_t* typec = reader.Peek(4);
+ mp4_demuxer::AtomType type(reader.ReadU32());
+ MSE_DEBUGV(AtomParser ,"Checking atom:'%c%c%c%c' @ %u",
+ typec[0], typec[1], typec[2], typec[3],
+ (uint32_t)reader.Offset() - 8);
+ if (std::find(std::begin(validBoxes), std::end(validBoxes), type)
+ == std::end(validBoxes)) {
+ // No valid box found, no point continuing.
+ mLastInvalidBox[0] = typec[0];
+ mLastInvalidBox[1] = typec[1];
+ mLastInvalidBox[2] = typec[2];
+ mLastInvalidBox[3] = typec[3];
+ mLastInvalidBox[4] = '\0';
+ mValid = false;
+ break;
+ }
+ if (mInitOffset.isNothing() &&
+ mp4_demuxer::AtomType(type) == initAtom) {
+ mInitOffset = Some(reader.Offset());
+ }
+ if (mMediaOffset.isNothing() &&
+ mp4_demuxer::AtomType(type) == mediaAtom) {
+ mMediaOffset = Some(reader.Offset());
+ }
+ if (mInitOffset.isSome() && mMediaOffset.isSome()) {
+ // We have everything we need.
+ break;
+ }
+ if (size == 1) {
+ // 64 bits size.
+ if (!reader.CanReadType<uint64_t>()) {
+ break;
+ }
+ size = reader.ReadU64();
+ } else if (size == 0) {
+ // Atom extends to the end of the buffer, it can't have what we're
+ // looking for.
+ break;
+ }
+ if (reader.Remaining() < size - 8) {
+ // Incomplete atom.
+ break;
+ }
+ reader.Read(size - 8);
+ }
+ }
+
+ bool StartWithInitSegment() const
+ {
+ return mInitOffset.isSome() &&
+ (mMediaOffset.isNothing() || mInitOffset.ref() < mMediaOffset.ref());
+ }
+ bool StartWithMediaSegment() const
+ {
+ return mMediaOffset.isSome() &&
+ (mInitOffset.isNothing() || mMediaOffset.ref() < mInitOffset.ref());
+ }
+ bool IsValid() const { return mValid; }
+ const char* LastInvalidBox() const { return mLastInvalidBox; }
+ private:
+ Maybe<size_t> mInitOffset;
+ Maybe<size_t> mMediaOffset;
+ bool mValid = true;
+ char mLastInvalidBox[5];
+ };
+
+public:
+ MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
+ int64_t& aStart,
+ int64_t& aEnd) override
+ {
+ bool initSegment = NS_SUCCEEDED(IsInitSegmentPresent(aData));
+ if (initSegment) {
+ mResource = new SourceBufferResource(NS_LITERAL_CSTRING("video/mp4"));
+ mStream = new MP4Stream(mResource);
+ // We use a timestampOffset of 0 for ContainerParser, and require
+ // consumers of ParseStartAndEndTimestamps to add their timestamp offset
+ // manually. This allows the ContainerParser to be shared across different
+ // timestampOffsets.
+ mParser = new mp4_demuxer::MoofParser(mStream, 0, /* aIsAudio = */ false);
+ mInitData = new MediaByteBuffer();
+ } else if (!mStream || !mParser) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ mResource->AppendData(aData);
+ MediaByteRangeSet byteRanges;
+ byteRanges +=
+ MediaByteRange(int64_t(mParser->mOffset), mResource->GetLength());
+ mParser->RebuildFragmentedIndex(byteRanges);
+
+ if (initSegment || !HasCompleteInitData()) {
+ MediaByteRange& range = mParser->mInitRange;
+ if (range.Length()) {
+ mCompleteInitSegmentRange = range;
+ if (!mInitData->SetLength(range.Length(), fallible)) {
+ // Super unlikely OOM
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ char* buffer = reinterpret_cast<char*>(mInitData->Elements());
+ mResource->ReadFromCache(buffer, range.mStart, range.Length());
+ MSE_DEBUG(MP4ContainerParser ,"Stashed init of %u bytes.",
+ range.Length());
+ } else {
+ MSE_DEBUG(MP4ContainerParser, "Incomplete init found.");
+ }
+ mHasInitData = true;
+ }
+
+ mp4_demuxer::Interval<mp4_demuxer::Microseconds> compositionRange =
+ mParser->GetCompositionRange(byteRanges);
+
+ mCompleteMediaHeaderRange = mParser->FirstCompleteMediaHeader();
+ mCompleteMediaSegmentRange = mParser->FirstCompleteMediaSegment();
+ ErrorResult rv;
+ if (HasCompleteInitData()) {
+ mResource->EvictData(mParser->mOffset, mParser->mOffset, rv);
+ }
+ if (NS_WARN_IF(rv.Failed())) {
+ rv.SuppressException();
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (compositionRange.IsNull()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ aStart = compositionRange.start;
+ aEnd = compositionRange.end;
+ MSE_DEBUG(MP4ContainerParser, "[%lld, %lld]",
+ aStart, aEnd);
+ return NS_OK;
+ }
+
+ // Gaps of up to 35ms (marginally longer than a single frame at 30fps) are considered
+ // to be sequential frames.
+ int64_t GetRoundingError() override
+ {
+ return 35000;
+ }
+
+private:
+ RefPtr<MP4Stream> mStream;
+ nsAutoPtr<mp4_demuxer::MoofParser> mParser;
+};
+#endif // MOZ_FMP4
+
+#ifdef MOZ_FMP4
+class ADTSContainerParser : public ContainerParser {
+public:
+ explicit ADTSContainerParser(const nsACString& aType)
+ : ContainerParser(aType)
+ {}
+
+ typedef struct {
+ size_t header_length; // Length of just the initialization data.
+ size_t frame_length; // Includes header_length;
+ uint8_t aac_frames; // Number of AAC frames in the ADTS frame.
+ bool have_crc;
+ } Header;
+
+ /// Helper to parse the ADTS header, returning data we care about.
+ /// Returns true if the header is parsed successfully.
+ /// Returns false if the header is invalid or incomplete,
+ /// without modifying the passed-in Header object.
+ bool Parse(MediaByteBuffer* aData, Header& header)
+ {
+ MOZ_ASSERT(aData);
+
+ // ADTS initialization segments are just the packet header.
+ if (aData->Length() < 7) {
+ MSE_DEBUG(ADTSContainerParser, "buffer too short for header.");
+ return false;
+ }
+ // Check 0xfffx sync word plus layer 0.
+ if (((*aData)[0] != 0xff) || (((*aData)[1] & 0xf6) != 0xf0)) {
+ MSE_DEBUG(ADTSContainerParser, "no syncword.");
+ return false;
+ }
+ bool have_crc = !((*aData)[1] & 0x01);
+ if (have_crc && aData->Length() < 9) {
+ MSE_DEBUG(ADTSContainerParser, "buffer too short for header with crc.");
+ return false;
+ }
+ uint8_t frequency_index = ((*aData)[2] & 0x3c) >> 2;
+ MOZ_ASSERT(frequency_index < 16);
+ if (frequency_index == 15) {
+ MSE_DEBUG(ADTSContainerParser, "explicit frequency disallowed.");
+ return false;
+ }
+ size_t header_length = have_crc ? 9 : 7;
+ size_t data_length = (((*aData)[3] & 0x03) << 11) ||
+ (((*aData)[4] & 0xff) << 3) ||
+ (((*aData)[5] & 0xe0) >> 5);
+ uint8_t frames = ((*aData)[6] & 0x03) + 1;
+ MOZ_ASSERT(frames > 0);
+ MOZ_ASSERT(frames < 4);
+
+ // Return successfully parsed data.
+ header.header_length = header_length;
+ header.frame_length = header_length + data_length;
+ header.aac_frames = frames;
+ header.have_crc = have_crc;
+ return true;
+ }
+
+ MediaResult IsInitSegmentPresent(MediaByteBuffer* aData) override
+ {
+ // Call superclass for logging.
+ ContainerParser::IsInitSegmentPresent(aData);
+
+ Header header;
+ if (!Parse(aData, header)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ MSE_DEBUGV(ADTSContainerParser, "%llu byte frame %d aac frames%s",
+ (unsigned long long)header.frame_length, (int)header.aac_frames,
+ header.have_crc ? " crc" : "");
+
+ return NS_OK;
+ }
+
+ MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData) override
+ {
+ // Call superclass for logging.
+ ContainerParser::IsMediaSegmentPresent(aData);
+
+ // Make sure we have a header so we know how long the frame is.
+ // NB this assumes the media segment buffer starts with an
+ // initialization segment. Since every frame has an ADTS header
+ // this is a normal place to divide packets, but we can re-parse
+ // mInitData if we need to handle separate media segments.
+ Header header;
+ if (!Parse(aData, header)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ // We're supposed to return true as long as aData contains the
+ // start of a media segment, whether or not it's complete. So
+ // return true if we have any data beyond the header.
+ if (aData->Length() <= header.header_length) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We should have at least a partial frame.
+ return NS_OK;
+ }
+
+ MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
+ int64_t& aStart,
+ int64_t& aEnd) override
+ {
+ // ADTS header.
+ Header header;
+ if (!Parse(aData, header)) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mHasInitData = true;
+ mCompleteInitSegmentRange = MediaByteRange(0, int64_t(header.header_length));
+
+ // Cache raw header in case the caller wants a copy.
+ mInitData = new MediaByteBuffer(header.header_length);
+ mInitData->AppendElements(aData->Elements(), header.header_length);
+
+ // Check that we have enough data for the frame body.
+ if (aData->Length() < header.frame_length) {
+ MSE_DEBUGV(ADTSContainerParser, "Not enough data for %llu byte frame"
+ " in %llu byte buffer.",
+ (unsigned long long)header.frame_length,
+ (unsigned long long)(aData->Length()));
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ mCompleteMediaSegmentRange = MediaByteRange(header.header_length,
+ header.frame_length);
+ // The ADTS MediaSource Byte Stream Format document doesn't
+ // define media header. Just treat it the same as the whole
+ // media segment.
+ mCompleteMediaHeaderRange = mCompleteMediaSegmentRange;
+
+ MSE_DEBUG(ADTSContainerParser, "[%lld, %lld]",
+ aStart, aEnd);
+ // We don't update timestamps, regardless.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // Audio shouldn't have gaps.
+ // Especially when we generate the timestamps ourselves.
+ int64_t GetRoundingError() override
+ {
+ return 0;
+ }
+};
+#endif // MOZ_FMP4
+
+/*static*/ ContainerParser*
+ContainerParser::CreateForMIMEType(const nsACString& aType)
+{
+ if (aType.LowerCaseEqualsLiteral("video/webm") || aType.LowerCaseEqualsLiteral("audio/webm")) {
+ return new WebMContainerParser(aType);
+ }
+
+#ifdef MOZ_FMP4
+ if (aType.LowerCaseEqualsLiteral("video/mp4") || aType.LowerCaseEqualsLiteral("audio/mp4")) {
+ return new MP4ContainerParser(aType);
+ }
+ if (aType.LowerCaseEqualsLiteral("audio/aac")) {
+ return new ADTSContainerParser(aType);
+ }
+#endif
+
+ return new ContainerParser(aType);
+}
+
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/ContainerParser.h b/dom/media/mediasource/ContainerParser.h
new file mode 100644
index 000000000..e31ed7579
--- /dev/null
+++ b/dom/media/mediasource/ContainerParser.h
@@ -0,0 +1,90 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_CONTAINERPARSER_H_
+#define MOZILLA_CONTAINERPARSER_H_
+
+#include "mozilla/RefPtr.h"
+#include "nsString.h"
+#include "MediaResource.h"
+#include "MediaResult.h"
+
+namespace mozilla {
+
+class MediaByteBuffer;
+class SourceBufferResource;
+
+class ContainerParser {
+public:
+ explicit ContainerParser(const nsACString& aType);
+ virtual ~ContainerParser();
+
+ // Return true if aData starts with an initialization segment.
+ // The base implementation exists only for debug logging and is expected
+ // to be called first from the overriding implementation.
+ // Return NS_OK if segment is present, NS_ERROR_NOT_AVAILABLE if no sufficient
+ // data is currently available to make a determination. Any other value
+ // indicates an error.
+ virtual MediaResult IsInitSegmentPresent(MediaByteBuffer* aData);
+
+ // Return true if aData starts with a media segment.
+ // The base implementation exists only for debug logging and is expected
+ // to be called first from the overriding implementation.
+ // Return NS_OK if segment is present, NS_ERROR_NOT_AVAILABLE if no sufficient
+ // data is currently available to make a determination. Any other value
+ // indicates an error.
+ virtual MediaResult IsMediaSegmentPresent(MediaByteBuffer* aData);
+
+ // Parse aData to extract the start and end frame times from the media
+ // segment. aData may not start on a parser sync boundary. Return NS_OK
+ // if aStart and aEnd have been updated and NS_ERROR_NOT_AVAILABLE otherwise
+ // when no error were encountered.
+ virtual MediaResult ParseStartAndEndTimestamps(MediaByteBuffer* aData,
+ int64_t& aStart, int64_t& aEnd);
+
+ // Compare aLhs and rHs, considering any error that may exist in the
+ // timestamps from the format's base representation. Return true if aLhs
+ // == aRhs within the error epsilon.
+ bool TimestampsFuzzyEqual(int64_t aLhs, int64_t aRhs);
+
+ virtual int64_t GetRoundingError();
+
+ MediaByteBuffer* InitData();
+
+ bool HasInitData()
+ {
+ return mHasInitData;
+ }
+
+ // Return true if a complete initialization segment has been passed
+ // to ParseStartAndEndTimestamps(). The calls below to retrieve
+ // MediaByteRanges will be valid from when this call first succeeds.
+ bool HasCompleteInitData();
+ // Returns the byte range of the first complete init segment, or an empty
+ // range if not complete.
+ MediaByteRange InitSegmentRange();
+ // Returns the byte range of the first complete media segment header,
+ // or an empty range if not complete.
+ MediaByteRange MediaHeaderRange();
+ // Returns the byte range of the first complete media segment or an empty
+ // range if not complete.
+ MediaByteRange MediaSegmentRange();
+
+ static ContainerParser* CreateForMIMEType(const nsACString& aType);
+
+protected:
+ RefPtr<MediaByteBuffer> mInitData;
+ RefPtr<SourceBufferResource> mResource;
+ bool mHasInitData;
+ MediaByteRange mCompleteInitSegmentRange;
+ MediaByteRange mCompleteMediaHeaderRange;
+ MediaByteRange mCompleteMediaSegmentRange;
+ const nsCString mType;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_CONTAINERPARSER_H_ */
diff --git a/dom/media/mediasource/MediaSource.cpp b/dom/media/mediasource/MediaSource.cpp
new file mode 100644
index 000000000..af541bbbb
--- /dev/null
+++ b/dom/media/mediasource/MediaSource.cpp
@@ -0,0 +1,591 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaSource.h"
+
+#include "AsyncEventRunner.h"
+#include "DecoderTraits.h"
+#include "Benchmark.h"
+#include "DecoderDoctorDiagnostics.h"
+#include "MediaContentType.h"
+#include "MediaResult.h"
+#include "MediaSourceUtils.h"
+#include "SourceBuffer.h"
+#include "SourceBufferList.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/BindingDeclarations.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "mozilla/mozalloc.h"
+#include "nsDebug.h"
+#include "nsError.h"
+#include "nsIRunnable.h"
+#include "nsIScriptObjectPrincipal.h"
+#include "nsPIDOMWindow.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Logging.h"
+#include "nsServiceManagerUtils.h"
+#include "mozilla/gfx/gfxVars.h"
+#include "mozilla/Sprintf.h"
+
+#ifdef MOZ_WIDGET_ANDROID
+#include "AndroidBridge.h"
+#endif
+
+struct JSContext;
+class JSObject;
+
+mozilla::LogModule* GetMediaSourceLog()
+{
+ static mozilla::LazyLogModule sLogModule("MediaSource");
+ return sLogModule;
+}
+
+mozilla::LogModule* GetMediaSourceAPILog()
+{
+ static mozilla::LazyLogModule sLogModule("MediaSource");
+ return sLogModule;
+}
+
+#define MSE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, ("MediaSource(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define MSE_API(arg, ...) MOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug, ("MediaSource(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+// Arbitrary limit.
+static const unsigned int MAX_SOURCE_BUFFERS = 16;
+
+namespace mozilla {
+
+// Returns true if we should enable MSE webm regardless of preferences.
+// 1. If MP4/H264 isn't supported:
+// * Windows XP
+// * Windows Vista and Server 2008 without the optional "Platform Update Supplement"
+// * N/KN editions (Europe and Korea) of Windows 7/8/8.1/10 without the
+// optional "Windows Media Feature Pack"
+// 2. If H264 hardware acceleration is not available.
+// 3. The CPU is considered to be fast enough
+static bool
+IsWebMForced(DecoderDoctorDiagnostics* aDiagnostics)
+{
+ bool mp4supported =
+ DecoderTraits::IsMP4TypeAndEnabled(NS_LITERAL_CSTRING("video/mp4"),
+ aDiagnostics);
+ bool hwsupported = gfx::gfxVars::CanUseHardwareVideoDecoding();
+#ifdef MOZ_WIDGET_ANDROID
+ return !mp4supported || !hwsupported || VP9Benchmark::IsVP9DecodeFast() ||
+ java::HardwareCodecCapabilityUtils::HasHWVP9();
+#else
+ return !mp4supported || !hwsupported || VP9Benchmark::IsVP9DecodeFast();
+#endif
+}
+
+namespace dom {
+
+/* static */
+nsresult
+MediaSource::IsTypeSupported(const nsAString& aType, DecoderDoctorDiagnostics* aDiagnostics)
+{
+ if (aType.IsEmpty()) {
+ return NS_ERROR_DOM_TYPE_ERR;
+ }
+
+ MediaContentType contentType{aType};
+ if (!contentType.IsValid()) {
+ return NS_ERROR_DOM_TYPE_ERR;
+ }
+
+ if (DecoderTraits::CanHandleContentType(contentType, aDiagnostics)
+ == CANPLAY_NO) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+
+ // Now we know that this media type could be played.
+ // MediaSource imposes extra restrictions, and some prefs.
+ const nsACString& mimeType = contentType.GetMIMEType();
+ if (mimeType.EqualsASCII("video/mp4") || mimeType.EqualsASCII("audio/mp4")) {
+ if (!Preferences::GetBool("media.mediasource.mp4.enabled", false)) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ return NS_OK;
+ }
+ if (mimeType.EqualsASCII("video/webm")) {
+ if (!(Preferences::GetBool("media.mediasource.webm.enabled", false) ||
+ IsWebMForced(aDiagnostics))) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ return NS_OK;
+ }
+ if (mimeType.EqualsASCII("audio/webm")) {
+ if (!(Preferences::GetBool("media.mediasource.webm.enabled", false) ||
+ Preferences::GetBool("media.mediasource.webm.audio.enabled", true))) {
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+ }
+ return NS_OK;
+ }
+
+ return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
+}
+
+/* static */ already_AddRefed<MediaSource>
+MediaSource::Constructor(const GlobalObject& aGlobal,
+ ErrorResult& aRv)
+{
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aGlobal.GetAsSupports());
+ if (!window) {
+ aRv.Throw(NS_ERROR_UNEXPECTED);
+ return nullptr;
+ }
+
+ RefPtr<MediaSource> mediaSource = new MediaSource(window);
+ return mediaSource.forget();
+}
+
+MediaSource::~MediaSource()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("");
+ if (mDecoder) {
+ mDecoder->DetachMediaSource();
+ }
+}
+
+SourceBufferList*
+MediaSource::SourceBuffers()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mReadyState == MediaSourceReadyState::Closed, mSourceBuffers->IsEmpty());
+ return mSourceBuffers;
+}
+
+SourceBufferList*
+MediaSource::ActiveSourceBuffers()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT_IF(mReadyState == MediaSourceReadyState::Closed, mActiveSourceBuffers->IsEmpty());
+ return mActiveSourceBuffers;
+}
+
+MediaSourceReadyState
+MediaSource::ReadyState()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return mReadyState;
+}
+
+double
+MediaSource::Duration()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (mReadyState == MediaSourceReadyState::Closed) {
+ return UnspecifiedNaN<double>();
+ }
+ MOZ_ASSERT(mDecoder);
+ return mDecoder->GetDuration();
+}
+
+void
+MediaSource::SetDuration(double aDuration, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetDuration(aDuration=%f, ErrorResult)", aDuration);
+ if (aDuration < 0 || IsNaN(aDuration)) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ return;
+ }
+ if (mReadyState != MediaSourceReadyState::Open ||
+ mSourceBuffers->AnyUpdating()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ DurationChange(aDuration, aRv);
+}
+
+void
+MediaSource::SetDuration(double aDuration)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetDuration(aDuration=%f)", aDuration);
+ mDecoder->SetMediaSourceDuration(aDuration);
+}
+
+already_AddRefed<SourceBuffer>
+MediaSource::AddSourceBuffer(const nsAString& aType, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ DecoderDoctorDiagnostics diagnostics;
+ nsresult rv = IsTypeSupported(aType, &diagnostics);
+ diagnostics.StoreFormatDiagnostics(GetOwner()
+ ? GetOwner()->GetExtantDoc()
+ : nullptr,
+ aType, NS_SUCCEEDED(rv), __func__);
+ MSE_API("AddSourceBuffer(aType=%s)%s",
+ NS_ConvertUTF16toUTF8(aType).get(),
+ rv == NS_OK ? "" : " [not supported]");
+ if (NS_FAILED(rv)) {
+ aRv.Throw(rv);
+ return nullptr;
+ }
+ if (mSourceBuffers->Length() >= MAX_SOURCE_BUFFERS) {
+ aRv.Throw(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR);
+ return nullptr;
+ }
+ if (mReadyState != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+ MediaContentType contentType{aType};
+ if (!contentType.IsValid()) {
+ aRv.Throw(NS_ERROR_DOM_NOT_SUPPORTED_ERR);
+ return nullptr;
+ }
+ const nsACString& mimeType = contentType.GetMIMEType();
+ RefPtr<SourceBuffer> sourceBuffer = new SourceBuffer(this, mimeType);
+ if (!sourceBuffer) {
+ aRv.Throw(NS_ERROR_FAILURE); // XXX need a better error here
+ return nullptr;
+ }
+ mSourceBuffers->Append(sourceBuffer);
+ MSE_DEBUG("sourceBuffer=%p", sourceBuffer.get());
+ return sourceBuffer.forget();
+}
+
+void
+MediaSource::SourceBufferIsActive(SourceBuffer* aSourceBuffer)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mActiveSourceBuffers->ClearSimple();
+ bool found = false;
+ for (uint32_t i = 0; i < mSourceBuffers->Length(); i++) {
+ SourceBuffer* sourceBuffer = mSourceBuffers->IndexedGetter(i, found);
+ MOZ_ALWAYS_TRUE(found);
+ if (sourceBuffer == aSourceBuffer) {
+ mActiveSourceBuffers->Append(aSourceBuffer);
+ } else if (sourceBuffer->IsActive()) {
+ mActiveSourceBuffers->AppendSimple(sourceBuffer);
+ }
+ }
+}
+
+void
+MediaSource::RemoveSourceBuffer(SourceBuffer& aSourceBuffer, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ SourceBuffer* sourceBuffer = &aSourceBuffer;
+ MSE_API("RemoveSourceBuffer(aSourceBuffer=%p)", sourceBuffer);
+ if (!mSourceBuffers->Contains(sourceBuffer)) {
+ aRv.Throw(NS_ERROR_DOM_NOT_FOUND_ERR);
+ return;
+ }
+
+ sourceBuffer->AbortBufferAppend();
+ // TODO:
+ // abort stream append loop (if running)
+
+ // TODO:
+ // For all sourceBuffer audioTracks, videoTracks, textTracks:
+ // set sourceBuffer to null
+ // remove sourceBuffer video, audio, text Tracks from MediaElement tracks
+ // remove sourceBuffer video, audio, text Tracks and fire "removetrack" at affected lists
+ // fire "removetrack" at modified MediaElement track lists
+ // If removed enabled/selected, fire "change" at affected MediaElement list.
+ if (mActiveSourceBuffers->Contains(sourceBuffer)) {
+ mActiveSourceBuffers->Remove(sourceBuffer);
+ }
+ mSourceBuffers->Remove(sourceBuffer);
+ // TODO: Free all resources associated with sourceBuffer
+}
+
+void
+MediaSource::EndOfStream(const Optional<MediaSourceEndOfStreamError>& aError, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("EndOfStream(aError=%d)",
+ aError.WasPassed() ? uint32_t(aError.Value()) : 0);
+ if (mReadyState != MediaSourceReadyState::Open ||
+ mSourceBuffers->AnyUpdating()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ SetReadyState(MediaSourceReadyState::Ended);
+ mSourceBuffers->Ended();
+ if (!aError.WasPassed()) {
+ DurationChange(mSourceBuffers->GetHighestBufferedEndTime(), aRv);
+ // Notify reader that all data is now available.
+ mDecoder->Ended(true);
+ return;
+ }
+ switch (aError.Value()) {
+ case MediaSourceEndOfStreamError::Network:
+ mDecoder->NetworkError();
+ break;
+ case MediaSourceEndOfStreamError::Decode:
+ mDecoder->DecodeError(NS_ERROR_DOM_MEDIA_FATAL_ERR);
+ break;
+ default:
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ }
+}
+
+void
+MediaSource::EndOfStream(const MediaResult& aError)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("EndOfStream(aError=%d)", aError.Code());
+
+ SetReadyState(MediaSourceReadyState::Ended);
+ mSourceBuffers->Ended();
+ mDecoder->DecodeError(aError);
+}
+
+/* static */ bool
+MediaSource::IsTypeSupported(const GlobalObject& aOwner, const nsAString& aType)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ DecoderDoctorDiagnostics diagnostics;
+ nsresult rv = IsTypeSupported(aType, &diagnostics);
+ nsCOMPtr<nsPIDOMWindowInner> window = do_QueryInterface(aOwner.GetAsSupports());
+ diagnostics.StoreFormatDiagnostics(window ? window->GetExtantDoc() : nullptr,
+ aType, NS_SUCCEEDED(rv), __func__);
+#define this nullptr
+ MSE_API("IsTypeSupported(aType=%s)%s ",
+ NS_ConvertUTF16toUTF8(aType).get(), rv == NS_OK ? "OK" : "[not supported]");
+#undef this // don't ever remove this line !
+ return NS_SUCCEEDED(rv);
+}
+
+/* static */ bool
+MediaSource::Enabled(JSContext* cx, JSObject* aGlobal)
+{
+ return Preferences::GetBool("media.mediasource.enabled");
+}
+
+void
+MediaSource::SetLiveSeekableRange(double aStart, double aEnd, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. If the readyState attribute is not "open" then throw an InvalidStateError
+ // exception and abort these steps.
+ if (mReadyState != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 2. If start is negative or greater than end, then throw a TypeError
+ // exception and abort these steps.
+ if (aStart < 0 || aStart > aEnd) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ return;
+ }
+
+ // 3. Set live seekable range to be a new normalized TimeRanges object
+ // containing a single range whose start position is start and end position is
+ // end.
+ mLiveSeekableRange =
+ Some(media::TimeInterval(media::TimeUnit::FromSeconds(aStart),
+ media::TimeUnit::FromSeconds(aEnd)));
+}
+
+void
+MediaSource::ClearLiveSeekableRange(ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // 1. If the readyState attribute is not "open" then throw an InvalidStateError
+ // exception and abort these steps.
+ if (mReadyState != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 2. If live seekable range contains a range, then set live seekable range to
+ // be a new empty TimeRanges object.
+ mLiveSeekableRange.reset();
+}
+
+bool
+MediaSource::Attach(MediaSourceDecoder* aDecoder)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Attach(aDecoder=%p) owner=%p", aDecoder, aDecoder->GetOwner());
+ MOZ_ASSERT(aDecoder);
+ MOZ_ASSERT(aDecoder->GetOwner());
+ if (mReadyState != MediaSourceReadyState::Closed) {
+ return false;
+ }
+ MOZ_ASSERT(!mMediaElement);
+ mMediaElement = aDecoder->GetOwner()->GetMediaElement();
+ MOZ_ASSERT(!mDecoder);
+ mDecoder = aDecoder;
+ mDecoder->AttachMediaSource(this);
+ SetReadyState(MediaSourceReadyState::Open);
+ return true;
+}
+
+void
+MediaSource::Detach()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("mDecoder=%p owner=%p",
+ mDecoder.get(), mDecoder ? mDecoder->GetOwner() : nullptr);
+ if (!mDecoder) {
+ MOZ_ASSERT(mReadyState == MediaSourceReadyState::Closed);
+ MOZ_ASSERT(mActiveSourceBuffers->IsEmpty() && mSourceBuffers->IsEmpty());
+ return;
+ }
+ mMediaElement = nullptr;
+ SetReadyState(MediaSourceReadyState::Closed);
+ if (mActiveSourceBuffers) {
+ mActiveSourceBuffers->Clear();
+ }
+ if (mSourceBuffers) {
+ mSourceBuffers->Clear();
+ }
+ mDecoder->DetachMediaSource();
+ mDecoder = nullptr;
+}
+
+MediaSource::MediaSource(nsPIDOMWindowInner* aWindow)
+ : DOMEventTargetHelper(aWindow)
+ , mDecoder(nullptr)
+ , mPrincipal(nullptr)
+ , mReadyState(MediaSourceReadyState::Closed)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers = new SourceBufferList(this);
+ mActiveSourceBuffers = new SourceBufferList(this);
+
+ nsCOMPtr<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow);
+ if (sop) {
+ mPrincipal = sop->GetPrincipal();
+ }
+
+ MSE_API("MediaSource(aWindow=%p) mSourceBuffers=%p mActiveSourceBuffers=%p",
+ aWindow, mSourceBuffers.get(), mActiveSourceBuffers.get());
+}
+
+void
+MediaSource::SetReadyState(MediaSourceReadyState aState)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aState != mReadyState);
+ MSE_DEBUG("SetReadyState(aState=%d) mReadyState=%d", aState, mReadyState);
+
+ MediaSourceReadyState oldState = mReadyState;
+ mReadyState = aState;
+
+ if (mReadyState == MediaSourceReadyState::Open &&
+ (oldState == MediaSourceReadyState::Closed ||
+ oldState == MediaSourceReadyState::Ended)) {
+ QueueAsyncSimpleEvent("sourceopen");
+ if (oldState == MediaSourceReadyState::Ended) {
+ // Notify reader that more data may come.
+ mDecoder->Ended(false);
+ }
+ return;
+ }
+
+ if (mReadyState == MediaSourceReadyState::Ended &&
+ oldState == MediaSourceReadyState::Open) {
+ QueueAsyncSimpleEvent("sourceended");
+ return;
+ }
+
+ if (mReadyState == MediaSourceReadyState::Closed &&
+ (oldState == MediaSourceReadyState::Open ||
+ oldState == MediaSourceReadyState::Ended)) {
+ QueueAsyncSimpleEvent("sourceclose");
+ return;
+ }
+
+ NS_WARNING("Invalid MediaSource readyState transition");
+}
+
+void
+MediaSource::DispatchSimpleEvent(const char* aName)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Dispatch event '%s'", aName);
+ DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName));
+}
+
+void
+MediaSource::QueueAsyncSimpleEvent(const char* aName)
+{
+ MSE_DEBUG("Queuing event '%s'", aName);
+ nsCOMPtr<nsIRunnable> event = new AsyncEventRunner<MediaSource>(this, aName);
+ NS_DispatchToMainThread(event);
+}
+
+void
+MediaSource::DurationChange(double aNewDuration, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("DurationChange(aNewDuration=%f)", aNewDuration);
+
+ // 1. If the current value of duration is equal to new duration, then return.
+ if (mDecoder->GetDuration() == aNewDuration) {
+ return;
+ }
+
+ // 2. If new duration is less than the highest starting presentation timestamp
+ // of any buffered coded frames for all SourceBuffer objects in sourceBuffers,
+ // then throw an InvalidStateError exception and abort these steps.
+ if (aNewDuration < mSourceBuffers->HighestStartTime()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ // 3. Let highest end time be the largest track buffer ranges end time across
+ // all the track buffers across all SourceBuffer objects in sourceBuffers.
+ double highestEndTime = mSourceBuffers->HighestEndTime();
+ // 4. If new duration is less than highest end time, then
+ // 4.1 Update new duration to equal highest end time.
+ aNewDuration =
+ std::max(aNewDuration, highestEndTime);
+
+ // 5. Update the media duration to new duration and run the HTMLMediaElement
+ // duration change algorithm.
+ mDecoder->SetMediaSourceDuration(aNewDuration);
+}
+
+void
+MediaSource::GetMozDebugReaderData(nsAString& aString)
+{
+ mDecoder->GetMozDebugReaderData(aString);
+}
+
+nsPIDOMWindowInner*
+MediaSource::GetParentObject() const
+{
+ return GetOwner();
+}
+
+JSObject*
+MediaSource::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return MediaSourceBinding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaSource, DOMEventTargetHelper,
+ mMediaElement,
+ mSourceBuffers, mActiveSourceBuffers)
+
+NS_IMPL_ADDREF_INHERITED(MediaSource, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(MediaSource, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(MediaSource)
+ NS_INTERFACE_MAP_ENTRY(mozilla::dom::MediaSource)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+#undef MSE_DEBUG
+#undef MSE_API
+
+} // namespace dom
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSource.h b/dom/media/mediasource/MediaSource.h
new file mode 100644
index 000000000..0d2dc0588
--- /dev/null
+++ b/dom/media/mediasource/MediaSource.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_MediaSource_h_
+#define mozilla_dom_MediaSource_h_
+
+#include "MediaSourceDecoder.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/dom/MediaSourceBinding.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsID.h"
+#include "nsISupports.h"
+#include "nscore.h"
+#include "TimeUnits.h"
+
+struct JSContext;
+class JSObject;
+class nsPIDOMWindowInner;
+
+namespace mozilla {
+
+class ErrorResult;
+template <typename T> class AsyncEventRunner;
+class MediaResult;
+
+namespace dom {
+
+class GlobalObject;
+class SourceBuffer;
+class SourceBufferList;
+template <typename T> class Optional;
+
+#define MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID \
+ { 0x3839d699, 0x22c5, 0x439f, \
+ { 0x94, 0xca, 0x0e, 0x0b, 0x26, 0xf9, 0xca, 0xbf } }
+
+class MediaSource final : public DOMEventTargetHelper
+{
+public:
+ /** WebIDL Methods. */
+ static already_AddRefed<MediaSource>
+ Constructor(const GlobalObject& aGlobal,
+ ErrorResult& aRv);
+
+ SourceBufferList* SourceBuffers();
+ SourceBufferList* ActiveSourceBuffers();
+ MediaSourceReadyState ReadyState();
+
+ double Duration();
+ void SetDuration(double aDuration, ErrorResult& aRv);
+
+ already_AddRefed<SourceBuffer> AddSourceBuffer(const nsAString& aType, ErrorResult& aRv);
+ void RemoveSourceBuffer(SourceBuffer& aSourceBuffer, ErrorResult& aRv);
+
+ void EndOfStream(const Optional<MediaSourceEndOfStreamError>& aError, ErrorResult& aRv);
+ void EndOfStream(const MediaResult& aError);
+
+ void SetLiveSeekableRange(double aStart, double aEnd, ErrorResult& aRv);
+ void ClearLiveSeekableRange(ErrorResult& aRv);
+
+ static bool IsTypeSupported(const GlobalObject&, const nsAString& aType);
+ static nsresult IsTypeSupported(const nsAString& aType, DecoderDoctorDiagnostics* aDiagnostics);
+
+ static bool Enabled(JSContext* cx, JSObject* aGlobal);
+
+ IMPL_EVENT_HANDLER(sourceopen);
+ IMPL_EVENT_HANDLER(sourceended);
+ IMPL_EVENT_HANDLER(sourceclosed);
+
+ /** End WebIDL Methods. */
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(MediaSource, DOMEventTargetHelper)
+ NS_DECLARE_STATIC_IID_ACCESSOR(MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID)
+
+ nsPIDOMWindowInner* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ // Attach this MediaSource to Decoder aDecoder. Returns false if already attached.
+ bool Attach(MediaSourceDecoder* aDecoder);
+ void Detach();
+
+ // Set mReadyState to aState and fire the required events at the MediaSource.
+ void SetReadyState(MediaSourceReadyState aState);
+
+ // Used by SourceBuffer to call CreateSubDecoder.
+ MediaSourceDecoder* GetDecoder()
+ {
+ return mDecoder;
+ }
+
+ nsIPrincipal* GetPrincipal()
+ {
+ return mPrincipal;
+ }
+
+ // Returns a string describing the state of the MediaSource internal
+ // buffered data. Used for debugging purposes.
+ void GetMozDebugReaderData(nsAString& aString);
+
+ bool HasLiveSeekableRange() const { return mLiveSeekableRange.isSome(); }
+ media::TimeInterval LiveSeekableRange() const
+ {
+ return mLiveSeekableRange.value();
+ }
+
+private:
+ // SourceBuffer uses SetDuration and SourceBufferIsActive
+ friend class mozilla::dom::SourceBuffer;
+
+ ~MediaSource();
+
+ explicit MediaSource(nsPIDOMWindowInner* aWindow);
+
+ friend class AsyncEventRunner<MediaSource>;
+ void DispatchSimpleEvent(const char* aName);
+ void QueueAsyncSimpleEvent(const char* aName);
+
+ void DurationChange(double aNewDuration, ErrorResult& aRv);
+
+ // SetDuration with no checks.
+ void SetDuration(double aDuration);
+
+ // Mark SourceBuffer as active and rebuild ActiveSourceBuffers.
+ void SourceBufferIsActive(SourceBuffer* aSourceBuffer);
+
+ RefPtr<SourceBufferList> mSourceBuffers;
+ RefPtr<SourceBufferList> mActiveSourceBuffers;
+
+ RefPtr<MediaSourceDecoder> mDecoder;
+ // Ensures the media element remains alive to dispatch progress and
+ // durationchanged events.
+ RefPtr<HTMLMediaElement> mMediaElement;
+
+ RefPtr<nsIPrincipal> mPrincipal;
+
+ MediaSourceReadyState mReadyState;
+
+ Maybe<media::TimeInterval> mLiveSeekableRange;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(MediaSource, MOZILLA_DOM_MEDIASOURCE_IMPLEMENTATION_IID)
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_MediaSource_h_ */
diff --git a/dom/media/mediasource/MediaSourceDecoder.cpp b/dom/media/mediasource/MediaSourceDecoder.cpp
new file mode 100644
index 000000000..2e83bfd23
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDecoder.cpp
@@ -0,0 +1,361 @@
+/* -*- mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "MediaSourceDecoder.h"
+
+#include "mozilla/Logging.h"
+#include "mozilla/dom/HTMLMediaElement.h"
+#include "MediaDecoderStateMachine.h"
+#include "MediaShutdownManager.h"
+#include "MediaSource.h"
+#include "MediaSourceResource.h"
+#include "MediaSourceUtils.h"
+#include "VideoUtils.h"
+#include "MediaSourceDemuxer.h"
+#include "SourceBufferList.h"
+#include <algorithm>
+
+extern mozilla::LogModule* GetMediaSourceLog();
+
+#define MSE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, ("MediaSourceDecoder(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define MSE_DEBUGV(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Verbose, ("MediaSourceDecoder(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+using namespace mozilla::media;
+
+namespace mozilla {
+
+MediaSourceDecoder::MediaSourceDecoder(dom::HTMLMediaElement* aElement)
+ : MediaDecoder(aElement)
+ , mMediaSource(nullptr)
+ , mEnded(false)
+{
+ mExplicitDuration.Set(Some(UnspecifiedNaN<double>()));
+}
+
+MediaDecoder*
+MediaSourceDecoder::Clone(MediaDecoderOwner* aOwner)
+{
+ // TODO: Sort out cloning.
+ return nullptr;
+}
+
+MediaDecoderStateMachine*
+MediaSourceDecoder::CreateStateMachine()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mDemuxer = new MediaSourceDemuxer();
+ mReader = new MediaFormatReader(this, mDemuxer, GetVideoFrameContainer());
+ return new MediaDecoderStateMachine(this, mReader);
+}
+
+nsresult
+MediaSourceDecoder::Load(nsIStreamListener**)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!GetStateMachine());
+
+ nsresult rv = MediaShutdownManager::Instance().Register(this);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ return rv;
+ }
+
+ SetStateMachine(CreateStateMachine());
+ if (!GetStateMachine()) {
+ NS_WARNING("Failed to create state machine!");
+ return NS_ERROR_FAILURE;
+ }
+
+ rv = GetStateMachine()->Init(this);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ SetStateMachineParameters();
+ return NS_OK;
+}
+
+media::TimeIntervals
+MediaSourceDecoder::GetSeekable()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mMediaSource) {
+ NS_WARNING("MediaSource element isn't attached");
+ return media::TimeIntervals::Invalid();
+ }
+
+ media::TimeIntervals seekable;
+ double duration = mMediaSource->Duration();
+ if (IsNaN(duration)) {
+ // Return empty range.
+ } else if (duration > 0 && mozilla::IsInfinite(duration)) {
+ media::TimeIntervals buffered = GetBuffered();
+
+ // 1. If live seekable range is not empty:
+ if (mMediaSource->HasLiveSeekableRange()) {
+ // 1. Let union ranges be the union of live seekable range and the
+ // HTMLMediaElement.buffered attribute.
+ media::TimeIntervals unionRanges =
+ buffered + mMediaSource->LiveSeekableRange();
+ // 2. Return a single range with a start time equal to the earliest start
+ // time in union ranges and an end time equal to the highest end time in
+ // union ranges and abort these steps.
+ seekable +=
+ media::TimeInterval(unionRanges.GetStart(), unionRanges.GetEnd());
+ return seekable;
+ }
+
+ if (buffered.Length()) {
+ seekable +=
+ media::TimeInterval(media::TimeUnit::FromSeconds(0), buffered.GetEnd());
+ }
+ } else {
+ seekable += media::TimeInterval(media::TimeUnit::FromSeconds(0),
+ media::TimeUnit::FromSeconds(duration));
+ }
+ MSE_DEBUG("ranges=%s", DumpTimeRanges(seekable).get());
+ return seekable;
+}
+
+media::TimeIntervals
+MediaSourceDecoder::GetBuffered()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mMediaSource) {
+ NS_WARNING("MediaSource element isn't attached");
+ return media::TimeIntervals::Invalid();
+ }
+ dom::SourceBufferList* sourceBuffers = mMediaSource->ActiveSourceBuffers();
+ if (!sourceBuffers) {
+ // Media source object is shutting down.
+ return TimeIntervals();
+ }
+ media::TimeUnit highestEndTime;
+ nsTArray<media::TimeIntervals> activeRanges;
+ media::TimeIntervals buffered;
+
+ for (uint32_t i = 0; i < sourceBuffers->Length(); i++) {
+ bool found;
+ dom::SourceBuffer* sb = sourceBuffers->IndexedGetter(i, found);
+ MOZ_ASSERT(found);
+
+ activeRanges.AppendElement(sb->GetTimeIntervals());
+ highestEndTime =
+ std::max(highestEndTime, activeRanges.LastElement().GetEnd());
+ }
+
+ buffered +=
+ media::TimeInterval(media::TimeUnit::FromMicroseconds(0), highestEndTime);
+
+ for (auto& range : activeRanges) {
+ if (mEnded && range.Length()) {
+ // Set the end time on the last range to highestEndTime by adding a
+ // new range spanning the current end time to highestEndTime, which
+ // Normalize() will then merge with the old last range.
+ range +=
+ media::TimeInterval(range.GetEnd(), highestEndTime);
+ }
+ buffered.Intersection(range);
+ }
+
+ MSE_DEBUG("ranges=%s", DumpTimeRanges(buffered).get());
+ return buffered;
+}
+
+void
+MediaSourceDecoder::Shutdown()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Shutdown");
+ // Detach first so that TrackBuffers are unused on the main thread when
+ // shut down on the decode task queue.
+ if (mMediaSource) {
+ mMediaSource->Detach();
+ }
+ mDemuxer = nullptr;
+
+ MediaDecoder::Shutdown();
+}
+
+/*static*/
+already_AddRefed<MediaResource>
+MediaSourceDecoder::CreateResource(nsIPrincipal* aPrincipal)
+{
+ return RefPtr<MediaResource>(new MediaSourceResource(aPrincipal)).forget();
+}
+
+void
+MediaSourceDecoder::AttachMediaSource(dom::MediaSource* aMediaSource)
+{
+ MOZ_ASSERT(!mMediaSource && !GetStateMachine() && NS_IsMainThread());
+ mMediaSource = aMediaSource;
+}
+
+void
+MediaSourceDecoder::DetachMediaSource()
+{
+ MOZ_ASSERT(mMediaSource && NS_IsMainThread());
+ mMediaSource = nullptr;
+}
+
+void
+MediaSourceDecoder::Ended(bool aEnded)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ static_cast<MediaSourceResource*>(GetResource())->SetEnded(aEnded);
+ if (aEnded) {
+ // We want the MediaSourceReader to refresh its buffered range as it may
+ // have been modified (end lined up).
+ NotifyDataArrived();
+ }
+ mEnded = aEnded;
+}
+
+void
+MediaSourceDecoder::AddSizeOfResources(ResourceSizes* aSizes)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (GetDemuxer()) {
+ GetDemuxer()->AddSizeOfResources(aSizes);
+ }
+}
+
+void
+MediaSourceDecoder::SetInitialDuration(int64_t aDuration)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // Only use the decoded duration if one wasn't already
+ // set.
+ if (!mMediaSource || !IsNaN(ExplicitDuration())) {
+ return;
+ }
+ double duration = aDuration;
+ // A duration of -1 is +Infinity.
+ if (aDuration >= 0) {
+ duration /= USECS_PER_S;
+ }
+ SetMediaSourceDuration(duration);
+}
+
+void
+MediaSourceDecoder::SetMediaSourceDuration(double aDuration)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!IsShutdown());
+ if (aDuration >= 0) {
+ int64_t checkedDuration;
+ if (NS_FAILED(SecondsToUsecs(aDuration, checkedDuration))) {
+ // INT64_MAX is used as infinity by the state machine.
+ // We want a very bigger number, but not infinity.
+ checkedDuration = INT64_MAX - 1;
+ }
+ SetExplicitDuration(aDuration);
+ } else {
+ SetExplicitDuration(PositiveInfinity<double>());
+ }
+}
+
+void
+MediaSourceDecoder::GetMozDebugReaderData(nsAString& aString)
+{
+ if (mReader && mDemuxer) {
+ mReader->GetMozDebugReaderData(aString);
+ mDemuxer->GetMozDebugReaderData(aString);
+ }
+}
+
+double
+MediaSourceDecoder::GetDuration()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return ExplicitDuration();
+}
+
+MediaDecoderOwner::NextFrameStatus
+MediaSourceDecoder::NextFrameBufferedStatus()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mMediaSource ||
+ mMediaSource->ReadyState() == dom::MediaSourceReadyState::Closed) {
+ return MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
+ }
+
+ // Next frame hasn't been decoded yet.
+ // Use the buffered range to consider if we have the next frame available.
+ TimeUnit currentPosition = TimeUnit::FromMicroseconds(CurrentPosition());
+ TimeIntervals buffered = GetBuffered();
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ TimeInterval interval(
+ currentPosition,
+ currentPosition
+ + media::TimeUnit::FromMicroseconds(DEFAULT_NEXT_FRAME_AVAILABLE_BUFFERED));
+ return buffered.ContainsStrict(ClampIntervalToEnd(interval))
+ ? MediaDecoderOwner::NEXT_FRAME_AVAILABLE
+ : MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE;
+}
+
+bool
+MediaSourceDecoder::CanPlayThrough()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (NextFrameBufferedStatus() == MediaDecoderOwner::NEXT_FRAME_UNAVAILABLE) {
+ return false;
+ }
+
+ if (IsNaN(mMediaSource->Duration())) {
+ // Don't have any data yet.
+ return false;
+ }
+ TimeUnit duration = TimeUnit::FromSeconds(mMediaSource->Duration());
+ TimeUnit currentPosition = TimeUnit::FromMicroseconds(CurrentPosition());
+ if (duration.IsInfinite()) {
+ // We can't make an informed decision and just assume that it's a live stream
+ return true;
+ } else if (duration <= currentPosition) {
+ return true;
+ }
+ // If we have data up to the mediasource's duration or 30s ahead, we can
+ // assume that we can play without interruption.
+ TimeIntervals buffered = GetBuffered();
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ TimeUnit timeAhead =
+ std::min(duration, currentPosition + TimeUnit::FromSeconds(30));
+ TimeInterval interval(currentPosition, timeAhead);
+ return buffered.ContainsStrict(ClampIntervalToEnd(interval));
+}
+
+void
+MediaSourceDecoder::NotifyWaitingForKey()
+{
+ mWaitingForKeyEvent.Notify();
+}
+
+MediaEventSource<void>*
+MediaSourceDecoder::WaitingForKeyEvent()
+{
+ return &mWaitingForKeyEvent;
+}
+
+TimeInterval
+MediaSourceDecoder::ClampIntervalToEnd(const TimeInterval& aInterval)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (!mEnded) {
+ return aInterval;
+ }
+ TimeUnit duration = TimeUnit::FromSeconds(GetDuration());
+ if (duration < aInterval.mStart) {
+ return aInterval;
+ }
+ return TimeInterval(aInterval.mStart,
+ std::min(aInterval.mEnd, duration),
+ aInterval.mFuzz);
+}
+
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSourceDecoder.h b/dom/media/mediasource/MediaSourceDecoder.h
new file mode 100644
index 000000000..2bf0ad831
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDecoder.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIASOURCEDECODER_H_
+#define MOZILLA_MEDIASOURCEDECODER_H_
+
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "MediaDecoder.h"
+#include "MediaFormatReader.h"
+
+class nsIStreamListener;
+
+namespace mozilla {
+
+class MediaResource;
+class MediaDecoderStateMachine;
+class SourceBufferDecoder;
+class TrackBuffer;
+enum MSRangeRemovalAction : uint8_t;
+class MediaSourceDemuxer;
+
+namespace dom {
+
+class HTMLMediaElement;
+class MediaSource;
+
+} // namespace dom
+
+class MediaSourceDecoder : public MediaDecoder
+{
+public:
+ explicit MediaSourceDecoder(dom::HTMLMediaElement* aElement);
+
+ MediaDecoder* Clone(MediaDecoderOwner* aOwner) override;
+ MediaDecoderStateMachine* CreateStateMachine() override;
+ nsresult Load(nsIStreamListener**) override;
+ media::TimeIntervals GetSeekable() override;
+ media::TimeIntervals GetBuffered() override;
+
+ void Shutdown() override;
+
+ static already_AddRefed<MediaResource> CreateResource(nsIPrincipal* aPrincipal = nullptr);
+
+ void AttachMediaSource(dom::MediaSource* aMediaSource);
+ void DetachMediaSource();
+
+ void Ended(bool aEnded);
+
+ // Return the duration of the video in seconds.
+ double GetDuration() override;
+
+ void SetInitialDuration(int64_t aDuration);
+ void SetMediaSourceDuration(double aDuration);
+
+ MediaSourceDemuxer* GetDemuxer()
+ {
+ return mDemuxer;
+ }
+
+ // Returns a string describing the state of the MediaSource internal
+ // buffered data. Used for debugging purposes.
+ void GetMozDebugReaderData(nsAString& aString) override;
+
+ void AddSizeOfResources(ResourceSizes* aSizes) override;
+
+ MediaDecoderOwner::NextFrameStatus NextFrameBufferedStatus() override;
+ bool CanPlayThrough() override;
+
+ void NotifyWaitingForKey() override;
+
+ MediaEventSource<void>* WaitingForKeyEvent() override;
+
+private:
+ void DoSetMediaSourceDuration(double aDuration);
+ media::TimeInterval ClampIntervalToEnd(const media::TimeInterval& aInterval);
+
+ // The owning MediaSource holds a strong reference to this decoder, and
+ // calls Attach/DetachMediaSource on this decoder to set and clear
+ // mMediaSource.
+ dom::MediaSource* mMediaSource;
+ RefPtr<MediaSourceDemuxer> mDemuxer;
+ RefPtr<MediaFormatReader> mReader;
+ MediaEventProducer<void> mWaitingForKeyEvent;
+
+ bool mEnded;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_MEDIASOURCEDECODER_H_ */
diff --git a/dom/media/mediasource/MediaSourceDemuxer.cpp b/dom/media/mediasource/MediaSourceDemuxer.cpp
new file mode 100644
index 000000000..73950e1a8
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDemuxer.cpp
@@ -0,0 +1,499 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include <limits>
+#include <stdint.h>
+
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "SourceBufferList.h"
+#include "nsPrintfCString.h"
+#include "OpusDecoder.h"
+
+namespace mozilla {
+
+typedef TrackInfo::TrackType TrackType;
+using media::TimeUnit;
+using media::TimeIntervals;
+
+MediaSourceDemuxer::MediaSourceDemuxer()
+ : mTaskQueue(new AutoTaskQueue(GetMediaThreadPool(MediaThreadType::PLAYBACK),
+ /* aSupportsTailDispatch = */ false))
+ , mMonitor("MediaSourceDemuxer")
+{
+ MOZ_ASSERT(NS_IsMainThread());
+}
+
+// Due to inaccuracies in determining buffer end
+// frames (Bug 1065207). This value is based on videos seen in the wild.
+const TimeUnit MediaSourceDemuxer::EOS_FUZZ = media::TimeUnit::FromMicroseconds(500000);
+
+RefPtr<MediaSourceDemuxer::InitPromise>
+MediaSourceDemuxer::Init()
+{
+ return InvokeAsync(GetTaskQueue(), this, __func__,
+ &MediaSourceDemuxer::AttemptInit);
+}
+
+RefPtr<MediaSourceDemuxer::InitPromise>
+MediaSourceDemuxer::AttemptInit()
+{
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (ScanSourceBuffersForContent()) {
+ return InitPromise::CreateAndResolve(NS_OK, __func__);
+ }
+
+ RefPtr<InitPromise> p = mInitPromise.Ensure(__func__);
+
+ return p;
+}
+
+void
+MediaSourceDemuxer::AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // NB: The track buffers must only be accessed on the TaskQueue.
+ RefPtr<MediaSourceDemuxer> self = this;
+ RefPtr<MediaSourceDecoder::ResourceSizes> sizes = aSizes;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction([self, sizes] () {
+ for (TrackBuffersManager* manager : self->mSourceBuffers) {
+ manager->AddSizeOfResources(sizes);
+ }
+ });
+
+ GetTaskQueue()->Dispatch(task.forget());
+}
+
+void MediaSourceDemuxer::NotifyDataArrived()
+{
+ RefPtr<MediaSourceDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction([self] () {
+ if (self->mInitPromise.IsEmpty()) {
+ return;
+ }
+ if (self->ScanSourceBuffersForContent()) {
+ self->mInitPromise.ResolveIfExists(NS_OK, __func__);
+ }
+ });
+ GetTaskQueue()->Dispatch(task.forget());
+}
+
+bool
+MediaSourceDemuxer::ScanSourceBuffersForContent()
+{
+ MOZ_ASSERT(OnTaskQueue());
+
+ if (mSourceBuffers.IsEmpty()) {
+ return false;
+ }
+
+ MonitorAutoLock mon(mMonitor);
+
+ bool haveEmptySourceBuffer = false;
+ for (const auto& sourceBuffer : mSourceBuffers) {
+ MediaInfo info = sourceBuffer->GetMetadata();
+ if (!info.HasAudio() && !info.HasVideo()) {
+ haveEmptySourceBuffer = true;
+ }
+ if (info.HasAudio() && !mAudioTrack) {
+ mInfo.mAudio = info.mAudio;
+ mAudioTrack = sourceBuffer;
+ }
+ if (info.HasVideo() && !mVideoTrack) {
+ mInfo.mVideo = info.mVideo;
+ mVideoTrack = sourceBuffer;
+ }
+ if (info.IsEncrypted() && !mInfo.IsEncrypted()) {
+ mInfo.mCrypto = info.mCrypto;
+ }
+ }
+ if (mInfo.HasAudio() && mInfo.HasVideo()) {
+ // We have both audio and video. We can ignore non-ready source buffer.
+ return true;
+ }
+ return !haveEmptySourceBuffer;
+}
+
+bool
+MediaSourceDemuxer::HasTrackType(TrackType aType) const
+{
+ MonitorAutoLock mon(mMonitor);
+
+ switch (aType) {
+ case TrackType::kAudioTrack:
+ return mInfo.HasAudio();
+ case TrackType::kVideoTrack:
+ return mInfo.HasVideo();
+ default:
+ return false;
+ }
+}
+
+uint32_t
+MediaSourceDemuxer::GetNumberTracks(TrackType aType) const
+{
+ return HasTrackType(aType) ? 1u : 0;
+}
+
+already_AddRefed<MediaTrackDemuxer>
+MediaSourceDemuxer::GetTrackDemuxer(TrackType aType, uint32_t aTrackNumber)
+{
+ RefPtr<TrackBuffersManager> manager = GetManager(aType);
+ if (!manager) {
+ return nullptr;
+ }
+ RefPtr<MediaSourceTrackDemuxer> e =
+ new MediaSourceTrackDemuxer(this, aType, manager);
+ mDemuxers.AppendElement(e);
+ return e.forget();
+}
+
+bool
+MediaSourceDemuxer::IsSeekable() const
+{
+ return true;
+}
+
+UniquePtr<EncryptionInfo>
+MediaSourceDemuxer::GetCrypto()
+{
+ MonitorAutoLock mon(mMonitor);
+ auto crypto = MakeUnique<EncryptionInfo>();
+ *crypto = mInfo.mCrypto;
+ return crypto;
+}
+
+void
+MediaSourceDemuxer::AttachSourceBuffer(TrackBuffersManager* aSourceBuffer)
+{
+ nsCOMPtr<nsIRunnable> task =
+ NewRunnableMethod<TrackBuffersManager*>(
+ this, &MediaSourceDemuxer::DoAttachSourceBuffer,
+ aSourceBuffer);
+ GetTaskQueue()->Dispatch(task.forget());
+}
+
+void
+MediaSourceDemuxer::DoAttachSourceBuffer(mozilla::TrackBuffersManager* aSourceBuffer)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ mSourceBuffers.AppendElement(aSourceBuffer);
+ ScanSourceBuffersForContent();
+}
+
+void
+MediaSourceDemuxer::DetachSourceBuffer(TrackBuffersManager* aSourceBuffer)
+{
+ nsCOMPtr<nsIRunnable> task =
+ NewRunnableMethod<TrackBuffersManager*>(
+ this, &MediaSourceDemuxer::DoDetachSourceBuffer,
+ aSourceBuffer);
+ GetTaskQueue()->Dispatch(task.forget());
+}
+
+void
+MediaSourceDemuxer::DoDetachSourceBuffer(TrackBuffersManager* aSourceBuffer)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); i++) {
+ if (mSourceBuffers[i].get() == aSourceBuffer) {
+ mSourceBuffers.RemoveElementAt(i);
+ }
+ }
+ if (aSourceBuffer == mAudioTrack) {
+ mAudioTrack = nullptr;
+ }
+ if (aSourceBuffer == mVideoTrack) {
+ mVideoTrack = nullptr;
+ }
+ ScanSourceBuffersForContent();
+}
+
+TrackInfo*
+MediaSourceDemuxer::GetTrackInfo(TrackType aTrack)
+{
+ MonitorAutoLock mon(mMonitor);
+ switch (aTrack) {
+ case TrackType::kAudioTrack:
+ return &mInfo.mAudio;
+ case TrackType::kVideoTrack:
+ return &mInfo.mVideo;
+ default:
+ return nullptr;
+ }
+}
+
+TrackBuffersManager*
+MediaSourceDemuxer::GetManager(TrackType aTrack)
+{
+ MonitorAutoLock mon(mMonitor);
+ switch (aTrack) {
+ case TrackType::kAudioTrack:
+ return mAudioTrack;
+ case TrackType::kVideoTrack:
+ return mVideoTrack;
+ default:
+ return nullptr;
+ }
+}
+
+MediaSourceDemuxer::~MediaSourceDemuxer()
+{
+ mInitPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
+}
+
+void
+MediaSourceDemuxer::GetMozDebugReaderData(nsAString& aString)
+{
+ MonitorAutoLock mon(mMonitor);
+ nsAutoCString result;
+ result += nsPrintfCString("Dumping data for demuxer %p:\n", this);
+ if (mAudioTrack) {
+ result += nsPrintfCString("\tDumping Audio Track Buffer(%s): - mLastAudioTime: %f\n"
+ "\t\tNumSamples:%u Size:%u Evictable:%u NextGetSampleIndex:%u NextInsertionIndex:%d\n",
+ mAudioTrack->mAudioTracks.mInfo->mMimeType.get(),
+ mAudioTrack->mAudioTracks.mNextSampleTime.ToSeconds(),
+ mAudioTrack->mAudioTracks.mBuffers[0].Length(),
+ mAudioTrack->mAudioTracks.mSizeBuffer,
+ mAudioTrack->Evictable(TrackInfo::kAudioTrack),
+ mAudioTrack->mAudioTracks.mNextGetSampleIndex.valueOr(-1),
+ mAudioTrack->mAudioTracks.mNextInsertionIndex.valueOr(-1));
+
+ result += nsPrintfCString("\t\tBuffered: ranges=%s\n",
+ DumpTimeRanges(mAudioTrack->SafeBuffered(TrackInfo::kAudioTrack)).get());
+ }
+ if (mVideoTrack) {
+ result += nsPrintfCString("\tDumping Video Track Buffer(%s) - mLastVideoTime: %f\n"
+ "\t\tNumSamples:%u Size:%u Evictable:%u NextGetSampleIndex:%u NextInsertionIndex:%d\n",
+ mVideoTrack->mVideoTracks.mInfo->mMimeType.get(),
+ mVideoTrack->mVideoTracks.mNextSampleTime.ToSeconds(),
+ mVideoTrack->mVideoTracks.mBuffers[0].Length(),
+ mVideoTrack->mVideoTracks.mSizeBuffer,
+ mVideoTrack->Evictable(TrackInfo::kVideoTrack),
+ mVideoTrack->mVideoTracks.mNextGetSampleIndex.valueOr(-1),
+ mVideoTrack->mVideoTracks.mNextInsertionIndex.valueOr(-1));
+
+ result += nsPrintfCString("\t\tBuffered: ranges=%s\n",
+ DumpTimeRanges(mVideoTrack->SafeBuffered(TrackInfo::kVideoTrack)).get());
+ }
+ aString += NS_ConvertUTF8toUTF16(result);
+}
+
+MediaSourceTrackDemuxer::MediaSourceTrackDemuxer(MediaSourceDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ TrackBuffersManager* aManager)
+ : mParent(aParent)
+ , mManager(aManager)
+ , mType(aType)
+ , mMonitor("MediaSourceTrackDemuxer")
+ , mReset(true)
+ , mPreRoll(
+ TimeUnit::FromMicroseconds(
+ OpusDataDecoder::IsOpus(mParent->GetTrackInfo(mType)->mMimeType)
+ ? 80000 : 0))
+{
+}
+
+UniquePtr<TrackInfo>
+MediaSourceTrackDemuxer::GetInfo() const
+{
+ return mParent->GetTrackInfo(mType)->Clone();
+}
+
+RefPtr<MediaSourceTrackDemuxer::SeekPromise>
+MediaSourceTrackDemuxer::Seek(media::TimeUnit aTime)
+{
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &MediaSourceTrackDemuxer::DoSeek, aTime);
+}
+
+RefPtr<MediaSourceTrackDemuxer::SamplesPromise>
+MediaSourceTrackDemuxer::GetSamples(int32_t aNumSamples)
+{
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &MediaSourceTrackDemuxer::DoGetSamples, aNumSamples);
+}
+
+void
+MediaSourceTrackDemuxer::Reset()
+{
+ MOZ_ASSERT(mParent, "Called after BreackCycle()");
+ RefPtr<MediaSourceTrackDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction([self] () {
+ self->mNextSample.reset();
+ self->mReset = true;
+ self->mManager->Seek(self->mType, TimeUnit(), TimeUnit());
+ {
+ MonitorAutoLock mon(self->mMonitor);
+ self->mNextRandomAccessPoint =
+ self->mManager->GetNextRandomAccessPoint(self->mType,
+ MediaSourceDemuxer::EOS_FUZZ);
+ }
+ });
+ mParent->GetTaskQueue()->Dispatch(task.forget());
+}
+
+nsresult
+MediaSourceTrackDemuxer::GetNextRandomAccessPoint(media::TimeUnit* aTime)
+{
+ MonitorAutoLock mon(mMonitor);
+ *aTime = mNextRandomAccessPoint;
+ return NS_OK;
+}
+
+RefPtr<MediaSourceTrackDemuxer::SkipAccessPointPromise>
+MediaSourceTrackDemuxer::SkipToNextRandomAccessPoint(media::TimeUnit aTimeThreshold)
+{
+ return InvokeAsync(mParent->GetTaskQueue(), this, __func__,
+ &MediaSourceTrackDemuxer::DoSkipToNextRandomAccessPoint,
+ aTimeThreshold);
+}
+
+media::TimeIntervals
+MediaSourceTrackDemuxer::GetBuffered()
+{
+ return mManager->Buffered();
+}
+
+void
+MediaSourceTrackDemuxer::BreakCycles()
+{
+ RefPtr<MediaSourceTrackDemuxer> self = this;
+ nsCOMPtr<nsIRunnable> task =
+ NS_NewRunnableFunction([self]() {
+ self->mParent = nullptr;
+ self->mManager = nullptr;
+ } );
+ mParent->GetTaskQueue()->Dispatch(task.forget());
+}
+
+RefPtr<MediaSourceTrackDemuxer::SeekPromise>
+MediaSourceTrackDemuxer::DoSeek(media::TimeUnit aTime)
+{
+ TimeIntervals buffered = mManager->Buffered(mType);
+ // Fuzz factor represents a +/- threshold. So when seeking it allows the gap
+ // to be twice as big as the fuzz value. We only want to allow EOS_FUZZ gap.
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ TimeUnit seekTime = std::max(aTime - mPreRoll, TimeUnit::FromMicroseconds(0));
+
+ if (mManager->IsEnded() && seekTime >= buffered.GetEnd()) {
+ // We're attempting to seek past the end time. Cap seekTime so that we seek
+ // to the last sample instead.
+ seekTime =
+ std::max(mManager->HighestStartTime(mType) - mPreRoll,
+ TimeUnit::FromMicroseconds(0));
+ }
+ if (!buffered.ContainsWithStrictEnd(seekTime)) {
+ if (!buffered.ContainsWithStrictEnd(aTime)) {
+ // We don't have the data to seek to.
+ return SeekPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ __func__);
+ }
+ // Theoretically we should reject the promise with WAITING_FOR_DATA,
+ // however, to avoid unwanted regressions we assume that if at this time
+ // we don't have the wanted data it won't come later.
+ // Instead of using the pre-rolled time, use the earliest time available in
+ // the interval.
+ TimeIntervals::IndexType index = buffered.Find(aTime);
+ MOZ_ASSERT(index != TimeIntervals::NoIndex);
+ seekTime = buffered[index].mStart;
+ }
+ seekTime = mManager->Seek(mType, seekTime, MediaSourceDemuxer::EOS_FUZZ);
+ MediaResult result = NS_OK;
+ RefPtr<MediaRawData> sample =
+ mManager->GetSample(mType,
+ media::TimeUnit(),
+ result);
+ MOZ_ASSERT(NS_SUCCEEDED(result) && sample);
+ mNextSample = Some(sample);
+ mReset = false;
+ {
+ MonitorAutoLock mon(mMonitor);
+ mNextRandomAccessPoint =
+ mManager->GetNextRandomAccessPoint(mType, MediaSourceDemuxer::EOS_FUZZ);
+ }
+ return SeekPromise::CreateAndResolve(seekTime, __func__);
+}
+
+RefPtr<MediaSourceTrackDemuxer::SamplesPromise>
+MediaSourceTrackDemuxer::DoGetSamples(int32_t aNumSamples)
+{
+ if (mReset) {
+ // If a seek (or reset) was recently performed, we ensure that the data
+ // we are about to retrieve is still available.
+ TimeIntervals buffered = mManager->Buffered(mType);
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+
+ if (!buffered.Length() && mManager->IsEnded()) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_END_OF_STREAM,
+ __func__);
+ }
+ if (!buffered.ContainsWithStrictEnd(TimeUnit::FromMicroseconds(0))) {
+ return SamplesPromise::CreateAndReject(NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA,
+ __func__);
+ }
+ mReset = false;
+ }
+ RefPtr<MediaRawData> sample;
+ if (mNextSample) {
+ sample = mNextSample.ref();
+ mNextSample.reset();
+ } else {
+ MediaResult result = NS_OK;
+ sample = mManager->GetSample(mType, MediaSourceDemuxer::EOS_FUZZ, result);
+ if (!sample) {
+ if (result == NS_ERROR_DOM_MEDIA_END_OF_STREAM ||
+ result == NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA) {
+ return SamplesPromise::CreateAndReject(
+ (result == NS_ERROR_DOM_MEDIA_END_OF_STREAM && mManager->IsEnded())
+ ? NS_ERROR_DOM_MEDIA_END_OF_STREAM
+ : NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, __func__);
+ }
+ return SamplesPromise::CreateAndReject(result, __func__);
+ }
+ }
+ RefPtr<SamplesHolder> samples = new SamplesHolder;
+ samples->mSamples.AppendElement(sample);
+ if (mNextRandomAccessPoint.ToMicroseconds() <= sample->mTime) {
+ MonitorAutoLock mon(mMonitor);
+ mNextRandomAccessPoint =
+ mManager->GetNextRandomAccessPoint(mType, MediaSourceDemuxer::EOS_FUZZ);
+ }
+ return SamplesPromise::CreateAndResolve(samples, __func__);
+}
+
+RefPtr<MediaSourceTrackDemuxer::SkipAccessPointPromise>
+MediaSourceTrackDemuxer::DoSkipToNextRandomAccessPoint(media::TimeUnit aTimeThreadshold)
+{
+ uint32_t parsed = 0;
+ // Ensure that the data we are about to skip to is still available.
+ TimeIntervals buffered = mManager->Buffered(mType);
+ buffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ if (buffered.ContainsWithStrictEnd(aTimeThreadshold)) {
+ bool found;
+ parsed = mManager->SkipToNextRandomAccessPoint(mType,
+ aTimeThreadshold,
+ MediaSourceDemuxer::EOS_FUZZ,
+ found);
+ if (found) {
+ return SkipAccessPointPromise::CreateAndResolve(parsed, __func__);
+ }
+ }
+ SkipFailureHolder holder(
+ mManager->IsEnded() ? NS_ERROR_DOM_MEDIA_END_OF_STREAM :
+ NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA, parsed);
+ return SkipAccessPointPromise::CreateAndReject(holder, __func__);
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSourceDemuxer.h b/dom/media/mediasource/MediaSourceDemuxer.h
new file mode 100644
index 000000000..02c91e3bc
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceDemuxer.h
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if !defined(MediaSourceDemuxer_h_)
+#define MediaSourceDemuxer_h_
+
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h"
+#include "AutoTaskQueue.h"
+
+#include "MediaDataDemuxer.h"
+#include "MediaDecoderReader.h"
+#include "MediaResource.h"
+#include "MediaSource.h"
+#include "TrackBuffersManager.h"
+
+namespace mozilla {
+
+class MediaResult;
+class MediaSourceTrackDemuxer;
+
+class MediaSourceDemuxer : public MediaDataDemuxer
+{
+public:
+ explicit MediaSourceDemuxer();
+
+ RefPtr<InitPromise> Init() override;
+
+ bool HasTrackType(TrackInfo::TrackType aType) const override;
+
+ uint32_t GetNumberTracks(TrackInfo::TrackType aType) const override;
+
+ already_AddRefed<MediaTrackDemuxer> GetTrackDemuxer(TrackInfo::TrackType aType,
+ uint32_t aTrackNumber) override;
+
+ bool IsSeekable() const override;
+
+ UniquePtr<EncryptionInfo> GetCrypto() override;
+
+ bool ShouldComputeStartTime() const override { return false; }
+
+ void NotifyDataArrived() override;
+
+ /* interface for TrackBuffersManager */
+ void AttachSourceBuffer(TrackBuffersManager* aSourceBuffer);
+ void DetachSourceBuffer(TrackBuffersManager* aSourceBuffer);
+ AutoTaskQueue* GetTaskQueue() { return mTaskQueue; }
+
+ // Returns a string describing the state of the MediaSource internal
+ // buffered data. Used for debugging purposes.
+ void GetMozDebugReaderData(nsAString& aString);
+
+ void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes);
+
+ // Gap allowed between frames.
+ static const media::TimeUnit EOS_FUZZ;
+
+private:
+ ~MediaSourceDemuxer();
+ friend class MediaSourceTrackDemuxer;
+ // Scan source buffers and update information.
+ bool ScanSourceBuffersForContent();
+ RefPtr<InitPromise> AttemptInit();
+ TrackBuffersManager* GetManager(TrackInfo::TrackType aType);
+ TrackInfo* GetTrackInfo(TrackInfo::TrackType);
+ void DoAttachSourceBuffer(TrackBuffersManager* aSourceBuffer);
+ void DoDetachSourceBuffer(TrackBuffersManager* aSourceBuffer);
+ bool OnTaskQueue()
+ {
+ return !GetTaskQueue() || GetTaskQueue()->IsCurrentThreadIn();
+ }
+
+ RefPtr<AutoTaskQueue> mTaskQueue;
+ nsTArray<RefPtr<MediaSourceTrackDemuxer>> mDemuxers;
+
+ nsTArray<RefPtr<TrackBuffersManager>> mSourceBuffers;
+
+ MozPromiseHolder<InitPromise> mInitPromise;
+
+ // Monitor to protect members below across multiple threads.
+ mutable Monitor mMonitor;
+ RefPtr<TrackBuffersManager> mAudioTrack;
+ RefPtr<TrackBuffersManager> mVideoTrack;
+ MediaInfo mInfo;
+};
+
+class MediaSourceTrackDemuxer : public MediaTrackDemuxer
+{
+public:
+ MediaSourceTrackDemuxer(MediaSourceDemuxer* aParent,
+ TrackInfo::TrackType aType,
+ TrackBuffersManager* aManager);
+
+ UniquePtr<TrackInfo> GetInfo() const override;
+
+ RefPtr<SeekPromise> Seek(media::TimeUnit aTime) override;
+
+ RefPtr<SamplesPromise> GetSamples(int32_t aNumSamples = 1) override;
+
+ void Reset() override;
+
+ nsresult GetNextRandomAccessPoint(media::TimeUnit* aTime) override;
+
+ RefPtr<SkipAccessPointPromise> SkipToNextRandomAccessPoint(media::TimeUnit aTimeThreshold) override;
+
+ media::TimeIntervals GetBuffered() override;
+
+ void BreakCycles() override;
+
+ bool GetSamplesMayBlock() const override
+ {
+ return false;
+ }
+
+private:
+ RefPtr<SeekPromise> DoSeek(media::TimeUnit aTime);
+ RefPtr<SamplesPromise> DoGetSamples(int32_t aNumSamples);
+ RefPtr<SkipAccessPointPromise> DoSkipToNextRandomAccessPoint(media::TimeUnit aTimeThreadshold);
+ already_AddRefed<MediaRawData> GetSample(MediaResult& aError);
+ // Return the timestamp of the next keyframe after mLastSampleIndex.
+ media::TimeUnit GetNextRandomAccessPoint();
+
+ RefPtr<MediaSourceDemuxer> mParent;
+ RefPtr<TrackBuffersManager> mManager;
+ TrackInfo::TrackType mType;
+ // Monitor protecting members below accessed from multiple threads.
+ Monitor mMonitor;
+ media::TimeUnit mNextRandomAccessPoint;
+ Maybe<RefPtr<MediaRawData>> mNextSample;
+ // Set to true following a reset. Ensure that the next sample demuxed
+ // is available at position 0.
+ bool mReset;
+
+ // Amount of pre-roll time when seeking.
+ // Set to 80ms if track is Opus.
+ const media::TimeUnit mPreRoll;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/dom/media/mediasource/MediaSourceResource.h b/dom/media/mediasource/MediaSourceResource.h
new file mode 100644
index 000000000..3ce4ebb52
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceResource.h
@@ -0,0 +1,109 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIASOURCERESOURCE_H_
+#define MOZILLA_MEDIASOURCERESOURCE_H_
+
+#include "MediaResource.h"
+#include "mozilla/Monitor.h"
+#include "mozilla/Logging.h"
+
+extern mozilla::LogModule* GetMediaSourceLog();
+
+#define MSE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, ("MediaSourceResource(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+
+#define UNIMPLEMENTED() MSE_DEBUG("UNIMPLEMENTED FUNCTION at %s:%d", __FILE__, __LINE__)
+
+namespace mozilla {
+
+class MediaSourceResource final : public MediaResource
+{
+public:
+ explicit MediaSourceResource(nsIPrincipal* aPrincipal = nullptr)
+ : mPrincipal(aPrincipal)
+ , mMonitor("MediaSourceResource")
+ , mEnded(false)
+ {}
+
+ nsresult Close() override { return NS_OK; }
+ void Suspend(bool aCloseImmediately) override { UNIMPLEMENTED(); }
+ void Resume() override { UNIMPLEMENTED(); }
+ bool CanClone() override { UNIMPLEMENTED(); return false; }
+ already_AddRefed<MediaResource> CloneData(MediaResourceCallback*) override { UNIMPLEMENTED(); return nullptr; }
+ void SetReadMode(MediaCacheStream::ReadMode aMode) override { UNIMPLEMENTED(); }
+ void SetPlaybackRate(uint32_t aBytesPerSecond) override { UNIMPLEMENTED(); }
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes) override { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
+ int64_t Tell() override { UNIMPLEMENTED(); return -1; }
+ void Pin() override { UNIMPLEMENTED(); }
+ void Unpin() override { UNIMPLEMENTED(); }
+ double GetDownloadRate(bool* aIsReliable) override { UNIMPLEMENTED(); *aIsReliable = false; return 0; }
+ int64_t GetLength() override { UNIMPLEMENTED(); return -1; }
+ int64_t GetNextCachedData(int64_t aOffset) override { UNIMPLEMENTED(); return -1; }
+ int64_t GetCachedDataEnd(int64_t aOffset) override { UNIMPLEMENTED(); return -1; }
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { UNIMPLEMENTED(); return false; }
+ bool IsSuspendedByCache() override { UNIMPLEMENTED(); return false; }
+ bool IsSuspended() override { UNIMPLEMENTED(); return false; }
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount) override { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
+ nsresult Open(nsIStreamListener** aStreamListener) override { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
+
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override
+ {
+ return RefPtr<nsIPrincipal>(mPrincipal).forget();
+ }
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override
+ {
+ UNIMPLEMENTED();
+ aRanges += MediaByteRange(0, GetLength());
+ return NS_OK;
+ }
+
+ bool IsTransportSeekable() override { return true; }
+ const nsCString& GetContentType() const override { return mType; }
+
+ bool IsLiveStream() override
+ {
+ MonitorAutoLock mon(mMonitor);
+ return !mEnded;
+ }
+ void SetEnded(bool aEnded)
+ {
+ MonitorAutoLock mon(mMonitor);
+ mEnded = aEnded;
+ }
+
+ bool IsExpectingMoreData() override
+ {
+ MonitorAutoLock mon(mMonitor);
+ return !mEnded;
+ }
+
+private:
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
+ {
+ size_t size = MediaResource::SizeOfExcludingThis(aMallocSizeOf);
+ size += mType.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+
+ return size;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override
+ {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ RefPtr<nsIPrincipal> mPrincipal;
+ const nsCString mType;
+ Monitor mMonitor;
+ bool mEnded; // protected by mMonitor
+};
+
+} // namespace mozilla
+
+#undef MSE_DEBUG
+#undef UNIMPLEMENTED
+
+#endif /* MOZILLA_MEDIASOURCERESOURCE_H_ */
diff --git a/dom/media/mediasource/MediaSourceUtils.cpp b/dom/media/mediasource/MediaSourceUtils.cpp
new file mode 100644
index 000000000..49cb39a18
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceUtils.cpp
@@ -0,0 +1,34 @@
+/* -*- mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#include "MediaSourceUtils.h"
+
+#include "mozilla/Logging.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+nsCString
+DumpTimeRanges(const media::TimeIntervals& aRanges)
+{
+ nsCString dump;
+
+ dump = "[";
+
+ for (uint32_t i = 0; i < aRanges.Length(); ++i) {
+ if (i > 0) {
+ dump += ", ";
+ }
+ dump += nsPrintfCString("(%f, %f)",
+ aRanges.Start(i).ToSeconds(),
+ aRanges.End(i).ToSeconds());
+ }
+
+ dump += "]";
+
+ return dump;
+}
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/MediaSourceUtils.h b/dom/media/mediasource/MediaSourceUtils.h
new file mode 100644
index 000000000..2bda99f1b
--- /dev/null
+++ b/dom/media/mediasource/MediaSourceUtils.h
@@ -0,0 +1,19 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_MEDIASOURCEUTILS_H_
+#define MOZILLA_MEDIASOURCEUTILS_H_
+
+#include "nsString.h"
+#include "TimeUnits.h"
+
+namespace mozilla {
+
+nsCString DumpTimeRanges(const media::TimeIntervals& aRanges);
+
+} // namespace mozilla
+
+#endif /* MOZILLA_MEDIASOURCEUTILS_H_ */
diff --git a/dom/media/mediasource/ResourceQueue.cpp b/dom/media/mediasource/ResourceQueue.cpp
new file mode 100644
index 000000000..5ca29ad37
--- /dev/null
+++ b/dom/media/mediasource/ResourceQueue.cpp
@@ -0,0 +1,214 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ResourceQueue.h"
+#include "nsDeque.h"
+#include "MediaData.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Sprintf.h"
+
+extern mozilla::LogModule* GetSourceBufferResourceLog();
+
+#define SBR_DEBUG(arg, ...) MOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Debug, ("ResourceQueue(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define SBR_DEBUGV(arg, ...) MOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Verbose, ("ResourceQueue(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+ResourceItem::ResourceItem(MediaByteBuffer* aData)
+ : mData(aData)
+{
+}
+
+size_t
+ResourceItem::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const
+{
+ // size including this
+ size_t size = aMallocSizeOf(this);
+
+ // size excluding this
+ size += mData->ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ return size;
+}
+
+class ResourceQueueDeallocator : public nsDequeFunctor {
+ void* operator() (void* aObject) override {
+ delete static_cast<ResourceItem*>(aObject);
+ return nullptr;
+ }
+};
+
+ResourceQueue::ResourceQueue()
+ : nsDeque(new ResourceQueueDeallocator())
+ , mLogicalLength(0)
+ , mOffset(0)
+{
+}
+
+uint64_t
+ResourceQueue::GetOffset()
+{
+ return mOffset;
+}
+
+uint64_t
+ResourceQueue::GetLength()
+{
+ return mLogicalLength;
+}
+
+void
+ResourceQueue::CopyData(uint64_t aOffset, uint32_t aCount, char* aDest)
+{
+ uint32_t offset = 0;
+ uint32_t start = GetAtOffset(aOffset, &offset);
+ uint32_t end = std::min(GetAtOffset(aOffset + aCount, nullptr) + 1, uint32_t(GetSize()));
+ for (uint32_t i = start; i < end; ++i) {
+ ResourceItem* item = ResourceAt(i);
+ uint32_t bytes = std::min(aCount, uint32_t(item->mData->Length() - offset));
+ if (bytes != 0) {
+ memcpy(aDest, &(*item->mData)[offset], bytes);
+ offset = 0;
+ aCount -= bytes;
+ aDest += bytes;
+ }
+ }
+}
+
+void
+ResourceQueue::AppendItem(MediaByteBuffer* aData)
+{
+ mLogicalLength += aData->Length();
+ Push(new ResourceItem(aData));
+}
+
+uint32_t
+ResourceQueue::Evict(uint64_t aOffset, uint32_t aSizeToEvict,
+ ErrorResult& aRv)
+{
+ SBR_DEBUG("Evict(aOffset=%llu, aSizeToEvict=%u)",
+ aOffset, aSizeToEvict);
+ return EvictBefore(std::min(aOffset, mOffset + (uint64_t)aSizeToEvict), aRv);
+}
+
+uint32_t ResourceQueue::EvictBefore(uint64_t aOffset, ErrorResult& aRv)
+{
+ SBR_DEBUG("EvictBefore(%llu)", aOffset);
+ uint32_t evicted = 0;
+ while (ResourceItem* item = ResourceAt(0)) {
+ SBR_DEBUG("item=%p length=%d offset=%llu",
+ item, item->mData->Length(), mOffset);
+ if (item->mData->Length() + mOffset >= aOffset) {
+ if (aOffset <= mOffset) {
+ break;
+ }
+ uint32_t offset = aOffset - mOffset;
+ mOffset += offset;
+ evicted += offset;
+ RefPtr<MediaByteBuffer> data = new MediaByteBuffer;
+ if (!data->AppendElements(item->mData->Elements() + offset,
+ item->mData->Length() - offset,
+ fallible)) {
+ aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
+ return 0;
+ }
+
+ item->mData = data;
+ break;
+ }
+ mOffset += item->mData->Length();
+ evicted += item->mData->Length();
+ delete PopFront();
+ }
+ return evicted;
+}
+
+uint32_t
+ResourceQueue::EvictAll()
+{
+ SBR_DEBUG("EvictAll()");
+ uint32_t evicted = 0;
+ while (ResourceItem* item = ResourceAt(0)) {
+ SBR_DEBUG("item=%p length=%d offset=%llu",
+ item, item->mData->Length(), mOffset);
+ mOffset += item->mData->Length();
+ evicted += item->mData->Length();
+ delete PopFront();
+ }
+ return evicted;
+}
+
+size_t
+ResourceQueue::SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const
+{
+ // Calculate the size of the internal deque.
+ size_t size = nsDeque::SizeOfExcludingThis(aMallocSizeOf);
+
+ // Sum the ResourceItems.
+ for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
+ const ResourceItem* item = ResourceAt(i);
+ size += item->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return size;
+}
+
+#if defined(DEBUG)
+void
+ResourceQueue::Dump(const char* aPath)
+{
+ for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
+ ResourceItem* item = ResourceAt(i);
+
+ char buf[255];
+ SprintfLiteral(buf, "%s/%08u.bin", aPath, i);
+ FILE* fp = fopen(buf, "wb");
+ if (!fp) {
+ return;
+ }
+ fwrite(item->mData->Elements(), item->mData->Length(), 1, fp);
+ fclose(fp);
+ }
+}
+#endif
+
+ResourceItem*
+ResourceQueue::ResourceAt(uint32_t aIndex) const
+{
+ return static_cast<ResourceItem*>(ObjectAt(aIndex));
+}
+
+uint32_t
+ResourceQueue::GetAtOffset(uint64_t aOffset, uint32_t *aResourceOffset)
+{
+ MOZ_RELEASE_ASSERT(aOffset >= mOffset);
+ uint64_t offset = mOffset;
+ for (uint32_t i = 0; i < uint32_t(GetSize()); ++i) {
+ ResourceItem* item = ResourceAt(i);
+ // If the item contains the start of the offset we want to
+ // break out of the loop.
+ if (item->mData->Length() + offset > aOffset) {
+ if (aResourceOffset) {
+ *aResourceOffset = aOffset - offset;
+ }
+ return i;
+ }
+ offset += item->mData->Length();
+ }
+ return GetSize();
+}
+
+ResourceItem*
+ResourceQueue::PopFront()
+{
+ return static_cast<ResourceItem*>(nsDeque::PopFront());
+}
+
+#undef SBR_DEBUG
+#undef SBR_DEBUGV
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/ResourceQueue.h b/dom/media/mediasource/ResourceQueue.h
new file mode 100644
index 000000000..0617890c8
--- /dev/null
+++ b/dom/media/mediasource/ResourceQueue.h
@@ -0,0 +1,86 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_RESOURCEQUEUE_H_
+#define MOZILLA_RESOURCEQUEUE_H_
+
+#include "nsDeque.h"
+#include "MediaData.h"
+
+namespace mozilla {
+
+class ErrorResult;
+
+// A SourceBufferResource has a queue containing the data that is appended
+// to it. The queue holds instances of ResourceItem which is an array of the
+// bytes. Appending data to the SourceBufferResource pushes this onto the
+// queue.
+
+// Data is evicted once it reaches a size threshold. This pops the items off
+// the front of the queue and deletes it. If an eviction happens then the
+// MediaSource is notified (done in SourceBuffer::AppendData) which then
+// requests all SourceBuffers to evict data up to approximately the same
+// timepoint.
+
+struct ResourceItem {
+ explicit ResourceItem(MediaByteBuffer* aData);
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const;
+ RefPtr<MediaByteBuffer> mData;
+};
+
+class ResourceQueue : private nsDeque {
+public:
+ ResourceQueue();
+
+ // Returns the logical byte offset of the start of the data.
+ uint64_t GetOffset();
+
+ // Returns the length of all items in the queue plus the offset.
+ // This is the logical length of the resource.
+ uint64_t GetLength();
+
+ // Copies aCount bytes from aOffset in the queue into aDest.
+ void CopyData(uint64_t aOffset, uint32_t aCount, char* aDest);
+
+ void AppendItem(MediaByteBuffer* aData);
+
+ // Tries to evict at least aSizeToEvict from the queue up until
+ // aOffset. Returns amount evicted.
+ uint32_t Evict(uint64_t aOffset, uint32_t aSizeToEvict,
+ ErrorResult& aRv);
+
+ uint32_t EvictBefore(uint64_t aOffset, ErrorResult& aRv);
+
+ uint32_t EvictAll();
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const;
+
+#if defined(DEBUG)
+ void Dump(const char* aPath);
+#endif
+
+private:
+ ResourceItem* ResourceAt(uint32_t aIndex) const;
+
+ // Returns the index of the resource that contains the given
+ // logical offset. aResourceOffset will contain the offset into
+ // the resource at the given index returned if it is not null. If
+ // no such resource exists, returns GetSize() and aOffset is
+ // untouched.
+ uint32_t GetAtOffset(uint64_t aOffset, uint32_t *aResourceOffset);
+
+ ResourceItem* PopFront();
+
+ // Logical length of the resource.
+ uint64_t mLogicalLength;
+
+ // Logical offset into the resource of the first element in the queue.
+ uint64_t mOffset;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_RESOURCEQUEUE_H_ */
diff --git a/dom/media/mediasource/SourceBuffer.cpp b/dom/media/mediasource/SourceBuffer.cpp
new file mode 100644
index 000000000..de14efb58
--- /dev/null
+++ b/dom/media/mediasource/SourceBuffer.cpp
@@ -0,0 +1,596 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceBuffer.h"
+
+#include "AsyncEventRunner.h"
+#include "MediaData.h"
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/FloatingPoint.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/dom/MediaSourceBinding.h"
+#include "mozilla/dom/TimeRanges.h"
+#include "nsError.h"
+#include "nsIEventTarget.h"
+#include "nsIRunnable.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Logging.h"
+#include <time.h>
+#include "TimeUnits.h"
+
+// GetCurrentTime is defined in winbase.h as zero argument macro forwarding to
+// GetTickCount() and conflicts with MediaDecoder::GetCurrentTime implementation.
+#ifdef GetCurrentTime
+#undef GetCurrentTime
+#endif
+
+struct JSContext;
+class JSObject;
+
+extern mozilla::LogModule* GetMediaSourceLog();
+extern mozilla::LogModule* GetMediaSourceAPILog();
+
+#define MSE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, ("SourceBuffer(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+#define MSE_DEBUGV(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Verbose, ("SourceBuffer(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+#define MSE_API(arg, ...) MOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug, ("SourceBuffer(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+using media::TimeUnit;
+typedef SourceBufferAttributes::AppendState AppendState;
+
+namespace dom {
+
+void
+SourceBuffer::SetMode(SourceBufferAppendMode aMode, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetMode(aMode=%d)", aMode);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mCurrentAttributes.mGenerateTimestamps &&
+ aMode == SourceBufferAppendMode::Segments) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ return;
+ }
+ MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed);
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+ if (mCurrentAttributes.GetAppendState() == AppendState::PARSING_MEDIA_SEGMENT){
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+
+ if (aMode == SourceBufferAppendMode::Sequence) {
+ // Will set GroupStartTimestamp to GroupEndTimestamp.
+ mCurrentAttributes.RestartGroupStartTimestamp();
+ }
+
+ mCurrentAttributes.SetAppendMode(aMode);
+}
+
+void
+SourceBuffer::SetTimestampOffset(double aTimestampOffset, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetTimestampOffset(aTimestampOffset=%f)", aTimestampOffset);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ MOZ_ASSERT(mMediaSource->ReadyState() != MediaSourceReadyState::Closed);
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+ if (mCurrentAttributes.GetAppendState() == AppendState::PARSING_MEDIA_SEGMENT){
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ mCurrentAttributes.SetApparentTimestampOffset(aTimestampOffset);
+ if (mCurrentAttributes.GetAppendMode() == SourceBufferAppendMode::Sequence) {
+ mCurrentAttributes.SetGroupStartTimestamp(mCurrentAttributes.GetTimestampOffset());
+ }
+}
+
+TimeRanges*
+SourceBuffer::GetBuffered(ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
+ // 1. If this object has been removed from the sourceBuffers attribute of the parent media source then throw an InvalidStateError exception and abort these steps.
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+ bool rangeChanged = true;
+ media::TimeIntervals intersection = mTrackBuffersManager->Buffered();
+ MSE_DEBUGV("intersection=%s", DumpTimeRanges(intersection).get());
+ if (mBuffered) {
+ media::TimeIntervals currentValue(mBuffered);
+ rangeChanged = (intersection != currentValue);
+ MSE_DEBUGV("currentValue=%s", DumpTimeRanges(currentValue).get());
+ }
+ // 5. If intersection ranges does not contain the exact same range information as the current value of this attribute, then update the current value of this attribute to intersection ranges.
+ if (rangeChanged) {
+ mBuffered = new TimeRanges(ToSupports(this));
+ intersection.ToTimeRanges(mBuffered);
+ }
+ // 6. Return the current value of this attribute.
+ return mBuffered;
+}
+
+media::TimeIntervals
+SourceBuffer::GetTimeIntervals()
+{
+ return mTrackBuffersManager->Buffered();
+}
+
+void
+SourceBuffer::SetAppendWindowStart(double aAppendWindowStart, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetAppendWindowStart(aAppendWindowStart=%f)", aAppendWindowStart);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (aAppendWindowStart < 0 ||
+ aAppendWindowStart >= mCurrentAttributes.GetAppendWindowEnd()) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ return;
+ }
+ mCurrentAttributes.SetAppendWindowStart(aAppendWindowStart);
+}
+
+void
+SourceBuffer::SetAppendWindowEnd(double aAppendWindowEnd, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("SetAppendWindowEnd(aAppendWindowEnd=%f)", aAppendWindowEnd);
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (IsNaN(aAppendWindowEnd) ||
+ aAppendWindowEnd <= mCurrentAttributes.GetAppendWindowStart()) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ return;
+ }
+ mCurrentAttributes.SetAppendWindowEnd(aAppendWindowEnd);
+}
+
+void
+SourceBuffer::AppendBuffer(const ArrayBuffer& aData, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("AppendBuffer(ArrayBuffer)");
+ aData.ComputeLengthAndData();
+ AppendData(aData.Data(), aData.Length(), aRv);
+}
+
+void
+SourceBuffer::AppendBuffer(const ArrayBufferView& aData, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("AppendBuffer(ArrayBufferView)");
+ aData.ComputeLengthAndData();
+ AppendData(aData.Data(), aData.Length(), aRv);
+}
+
+void
+SourceBuffer::Abort(ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Abort()");
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mMediaSource->ReadyState() != MediaSourceReadyState::Open) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mPendingRemoval.Exists()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ AbortBufferAppend();
+ ResetParserState();
+ mCurrentAttributes.SetAppendWindowStart(0);
+ mCurrentAttributes.SetAppendWindowEnd(PositiveInfinity<double>());
+}
+
+void
+SourceBuffer::AbortBufferAppend()
+{
+ if (mUpdating) {
+ if (mPendingAppend.Exists()) {
+ mPendingAppend.Disconnect();
+ mTrackBuffersManager->AbortAppendData();
+ }
+ AbortUpdating();
+ }
+}
+
+void
+SourceBuffer::ResetParserState()
+{
+ mTrackBuffersManager->ResetParserState(mCurrentAttributes);
+}
+
+void
+SourceBuffer::Remove(double aStart, double aEnd, ErrorResult& aRv)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Remove(aStart=%f, aEnd=%f)", aStart, aEnd);
+ if (!IsAttached()) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return;
+ }
+ if (IsNaN(mMediaSource->Duration()) ||
+ aStart < 0 || aStart > mMediaSource->Duration() ||
+ aEnd <= aStart || IsNaN(aEnd)) {
+ aRv.Throw(NS_ERROR_DOM_TYPE_ERR);
+ return;
+ }
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+
+ RangeRemoval(aStart, aEnd);
+}
+
+void
+SourceBuffer::RangeRemoval(double aStart, double aEnd)
+{
+ StartUpdating();
+
+ RefPtr<SourceBuffer> self = this;
+ mPendingRemoval.Begin(
+ mTrackBuffersManager->RangeRemoval(TimeUnit::FromSeconds(aStart),
+ TimeUnit::FromSeconds(aEnd))
+ ->Then(AbstractThread::MainThread(), __func__,
+ [self] (bool) {
+ self->mPendingRemoval.Complete();
+ self->StopUpdating();
+ },
+ []() { MOZ_ASSERT(false); }));
+}
+
+void
+SourceBuffer::Detach()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Detach");
+ if (!mMediaSource) {
+ MSE_DEBUG("Already detached");
+ return;
+ }
+ AbortBufferAppend();
+ if (mTrackBuffersManager) {
+ mTrackBuffersManager->Detach();
+ mMediaSource->GetDecoder()->GetDemuxer()->DetachSourceBuffer(
+ mTrackBuffersManager.get());
+ }
+ mTrackBuffersManager = nullptr;
+ mMediaSource = nullptr;
+}
+
+void
+SourceBuffer::Ended()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(IsAttached());
+ MSE_DEBUG("Ended");
+ mTrackBuffersManager->Ended();
+}
+
+SourceBuffer::SourceBuffer(MediaSource* aMediaSource, const nsACString& aType)
+ : DOMEventTargetHelper(aMediaSource->GetParentObject())
+ , mMediaSource(aMediaSource)
+ , mCurrentAttributes(aType.LowerCaseEqualsLiteral("audio/mpeg") ||
+ aType.LowerCaseEqualsLiteral("audio/aac"))
+ , mUpdating(false)
+ , mActive(false)
+ , mType(aType)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(aMediaSource);
+
+ mTrackBuffersManager =
+ new TrackBuffersManager(aMediaSource->GetDecoder(), aType);
+
+ MSE_DEBUG("Create mTrackBuffersManager=%p",
+ mTrackBuffersManager.get());
+
+ ErrorResult dummy;
+ if (mCurrentAttributes.mGenerateTimestamps) {
+ SetMode(SourceBufferAppendMode::Sequence, dummy);
+ } else {
+ SetMode(SourceBufferAppendMode::Segments, dummy);
+ }
+ mMediaSource->GetDecoder()->GetDemuxer()->AttachSourceBuffer(
+ mTrackBuffersManager.get());
+}
+
+SourceBuffer::~SourceBuffer()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mMediaSource);
+ MSE_DEBUG("");
+}
+
+MediaSource*
+SourceBuffer::GetParentObject() const
+{
+ return mMediaSource;
+}
+
+JSObject*
+SourceBuffer::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return SourceBufferBinding::Wrap(aCx, this, aGivenProto);
+}
+
+void
+SourceBuffer::DispatchSimpleEvent(const char* aName)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Dispatch event '%s'", aName);
+ DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName));
+}
+
+void
+SourceBuffer::QueueAsyncSimpleEvent(const char* aName)
+{
+ MSE_DEBUG("Queuing event '%s'", aName);
+ nsCOMPtr<nsIRunnable> event = new AsyncEventRunner<SourceBuffer>(this, aName);
+ NS_DispatchToMainThread(event, NS_DISPATCH_NORMAL);
+}
+
+void
+SourceBuffer::StartUpdating()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!mUpdating);
+ mUpdating = true;
+ QueueAsyncSimpleEvent("updatestart");
+}
+
+void
+SourceBuffer::StopUpdating()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ if (!mUpdating) {
+ // The buffer append or range removal algorithm has been interrupted by
+ // abort().
+ return;
+ }
+ mUpdating = false;
+ QueueAsyncSimpleEvent("update");
+ QueueAsyncSimpleEvent("updateend");
+}
+
+void
+SourceBuffer::AbortUpdating()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mUpdating = false;
+ QueueAsyncSimpleEvent("abort");
+ QueueAsyncSimpleEvent("updateend");
+}
+
+void
+SourceBuffer::CheckEndTime()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ // Check if we need to update mMediaSource duration
+ double endTime = mCurrentAttributes.GetGroupEndTimestamp().ToSeconds();
+ double duration = mMediaSource->Duration();
+ if (endTime > duration) {
+ mMediaSource->SetDuration(endTime);
+ }
+}
+
+void
+SourceBuffer::AppendData(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv)
+{
+ MSE_DEBUG("AppendData(aLength=%u)", aLength);
+
+ RefPtr<MediaByteBuffer> data = PrepareAppend(aData, aLength, aRv);
+ if (!data) {
+ return;
+ }
+ StartUpdating();
+
+ mPendingAppend.Begin(mTrackBuffersManager->AppendData(data, mCurrentAttributes)
+ ->Then(AbstractThread::MainThread(), __func__, this,
+ &SourceBuffer::AppendDataCompletedWithSuccess,
+ &SourceBuffer::AppendDataErrored));
+}
+
+void
+SourceBuffer::AppendDataCompletedWithSuccess(SourceBufferTask::AppendBufferResult aResult)
+{
+ MOZ_ASSERT(mUpdating);
+ mPendingAppend.Complete();
+
+ if (aResult.first()) {
+ if (!mActive) {
+ mActive = true;
+ mMediaSource->SourceBufferIsActive(this);
+ }
+ }
+ if (mActive) {
+ // Tell our parent decoder that we have received new data.
+ mMediaSource->GetDecoder()->NotifyDataArrived();
+ // Send progress event.
+ mMediaSource->GetDecoder()->NotifyBytesDownloaded();
+ }
+
+ mCurrentAttributes = aResult.second();
+
+ CheckEndTime();
+
+ StopUpdating();
+}
+
+void
+SourceBuffer::AppendDataErrored(const MediaResult& aError)
+{
+ MOZ_ASSERT(mUpdating);
+ mPendingAppend.Complete();
+
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_CANCELED:
+ // Nothing further to do as the trackbuffer has been shutdown.
+ // or append was aborted and abort() has handled all the events.
+ break;
+ default:
+ AppendError(aError);
+ break;
+ }
+}
+
+void
+SourceBuffer::AppendError(const MediaResult& aDecodeError)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ResetParserState();
+
+ mUpdating = false;
+
+ QueueAsyncSimpleEvent("error");
+ QueueAsyncSimpleEvent("updateend");
+
+ MOZ_ASSERT(NS_FAILED(aDecodeError));
+
+ mMediaSource->EndOfStream(aDecodeError);
+}
+
+already_AddRefed<MediaByteBuffer>
+SourceBuffer::PrepareAppend(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv)
+{
+ typedef TrackBuffersManager::EvictDataResult Result;
+
+ if (!IsAttached() || mUpdating) {
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ // If the HTMLMediaElement.error attribute is not null, then throw an
+ // InvalidStateError exception and abort these steps.
+ if (!mMediaSource->GetDecoder() ||
+ mMediaSource->GetDecoder()->OwnerHasError()) {
+ MSE_DEBUG("HTMLMediaElement.error is not null");
+ aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
+ return nullptr;
+ }
+
+ if (mMediaSource->ReadyState() == MediaSourceReadyState::Ended) {
+ mMediaSource->SetReadyState(MediaSourceReadyState::Open);
+ }
+
+ // Eviction uses a byte threshold. If the buffer is greater than the
+ // number of bytes then data is evicted.
+ // TODO: Drive evictions off memory pressure notifications.
+ // TODO: Consider a global eviction threshold rather than per TrackBuffer.
+ // Give a chance to the TrackBuffersManager to evict some data if needed.
+ Result evicted =
+ mTrackBuffersManager->EvictData(TimeUnit::FromSeconds(mMediaSource->GetDecoder()->GetCurrentTime()),
+ aLength);
+
+ // See if we have enough free space to append our new data.
+ if (evicted == Result::BUFFER_FULL) {
+ aRv.Throw(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR);
+ return nullptr;
+ }
+
+ RefPtr<MediaByteBuffer> data = new MediaByteBuffer();
+ if (!data->AppendElements(aData, aLength, fallible)) {
+ aRv.Throw(NS_ERROR_DOM_QUOTA_EXCEEDED_ERR);
+ return nullptr;
+ }
+ return data.forget();
+}
+
+double
+SourceBuffer::GetBufferedStart()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ ErrorResult dummy;
+ RefPtr<TimeRanges> ranges = GetBuffered(dummy);
+ return ranges->Length() > 0 ? ranges->GetStartTime() : 0;
+}
+
+double
+SourceBuffer::GetBufferedEnd()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ ErrorResult dummy;
+ RefPtr<TimeRanges> ranges = GetBuffered(dummy);
+ return ranges->Length() > 0 ? ranges->GetEndTime() : 0;
+}
+
+double
+SourceBuffer::HighestStartTime()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return mTrackBuffersManager
+ ? mTrackBuffersManager->HighestStartTime().ToSeconds()
+ : 0.0;
+}
+
+double
+SourceBuffer::HighestEndTime()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return mTrackBuffersManager
+ ? mTrackBuffersManager->HighestEndTime().ToSeconds()
+ : 0.0;
+}
+
+NS_IMPL_CYCLE_COLLECTION_CLASS(SourceBuffer)
+
+NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SourceBuffer)
+ // Tell the TrackBuffer to end its current SourceBufferResource.
+ TrackBuffersManager* manager = tmp->mTrackBuffersManager;
+ if (manager) {
+ manager->Detach();
+ }
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_UNLINK(mBuffered)
+NS_IMPL_CYCLE_COLLECTION_UNLINK_END_INHERITED(DOMEventTargetHelper)
+
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(SourceBuffer,
+ DOMEventTargetHelper)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaSource)
+ NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mBuffered)
+NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
+
+NS_IMPL_ADDREF_INHERITED(SourceBuffer, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SourceBuffer, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(SourceBuffer)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+#undef MSE_API
+
+} // namespace dom
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/SourceBuffer.h b/dom/media/mediasource/SourceBuffer.h
new file mode 100644
index 000000000..440e8f60e
--- /dev/null
+++ b/dom/media/mediasource/SourceBuffer.h
@@ -0,0 +1,189 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SourceBuffer_h_
+#define mozilla_dom_SourceBuffer_h_
+
+#include "mozilla/MozPromise.h"
+#include "MediaSource.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "mozilla/UniquePtr.h"
+#include "mozilla/dom/SourceBufferBinding.h"
+#include "mozilla/dom/TypedArray.h"
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsString.h"
+#include "nscore.h"
+#include "TrackBuffersManager.h"
+#include "SourceBufferTask.h"
+
+class JSObject;
+struct JSContext;
+
+namespace mozilla {
+
+class ErrorResult;
+class MediaByteBuffer;
+template <typename T> class AsyncEventRunner;
+
+namespace dom {
+
+class TimeRanges;
+
+class SourceBuffer final : public DOMEventTargetHelper
+{
+public:
+ /** WebIDL Methods. */
+ SourceBufferAppendMode Mode() const
+ {
+ return mCurrentAttributes.GetAppendMode();
+ }
+
+ void SetMode(SourceBufferAppendMode aMode, ErrorResult& aRv);
+
+ bool Updating() const
+ {
+ return mUpdating;
+ }
+
+ TimeRanges* GetBuffered(ErrorResult& aRv);
+ media::TimeIntervals GetTimeIntervals();
+
+ double TimestampOffset() const
+ {
+ return mCurrentAttributes.GetApparentTimestampOffset();
+ }
+
+ void SetTimestampOffset(double aTimestampOffset, ErrorResult& aRv);
+
+ double AppendWindowStart() const
+ {
+ return mCurrentAttributes.GetAppendWindowStart();
+ }
+
+ void SetAppendWindowStart(double aAppendWindowStart, ErrorResult& aRv);
+
+ double AppendWindowEnd() const
+ {
+ return mCurrentAttributes.GetAppendWindowEnd();
+ }
+
+ void SetAppendWindowEnd(double aAppendWindowEnd, ErrorResult& aRv);
+
+ void AppendBuffer(const ArrayBuffer& aData, ErrorResult& aRv);
+ void AppendBuffer(const ArrayBufferView& aData, ErrorResult& aRv);
+
+ void Abort(ErrorResult& aRv);
+ void AbortBufferAppend();
+
+ void Remove(double aStart, double aEnd, ErrorResult& aRv);
+
+ IMPL_EVENT_HANDLER(updatestart);
+ IMPL_EVENT_HANDLER(update);
+ IMPL_EVENT_HANDLER(updateend);
+ IMPL_EVENT_HANDLER(error);
+ IMPL_EVENT_HANDLER(abort);
+
+ /** End WebIDL Methods. */
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SourceBuffer, DOMEventTargetHelper)
+
+ SourceBuffer(MediaSource* aMediaSource, const nsACString& aType);
+
+ MediaSource* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ // Notify the SourceBuffer that it has been detached from the
+ // MediaSource's sourceBuffer list.
+ void Detach();
+ bool IsAttached() const
+ {
+ return mMediaSource != nullptr;
+ }
+
+ void Ended();
+
+ double GetBufferedStart();
+ double GetBufferedEnd();
+ double HighestStartTime();
+ double HighestEndTime();
+
+ // Runs the range removal algorithm as defined by the MSE spec.
+ void RangeRemoval(double aStart, double aEnd);
+
+ bool IsActive() const
+ {
+ return mActive;
+ }
+
+private:
+ ~SourceBuffer();
+
+ friend class AsyncEventRunner<SourceBuffer>;
+ friend class BufferAppendRunnable;
+ friend class mozilla::TrackBuffersManager;
+ void DispatchSimpleEvent(const char* aName);
+ void QueueAsyncSimpleEvent(const char* aName);
+
+ // Update mUpdating and fire the appropriate events.
+ void StartUpdating();
+ void StopUpdating();
+ void AbortUpdating();
+ void ResetParserState();
+
+ // If the media segment contains data beyond the current duration,
+ // then run the duration change algorithm with new duration set to the
+ // maximum of the current duration and the group end timestamp.
+ void CheckEndTime();
+
+ // Shared implementation of AppendBuffer overloads.
+ void AppendData(const uint8_t* aData, uint32_t aLength, ErrorResult& aRv);
+
+ // Implement the "Append Error Algorithm".
+ // Will call endOfStream() with "decode" error if aDecodeError is true.
+ // 3.5.3 Append Error Algorithm
+ // http://w3c.github.io/media-source/#sourcebuffer-append-error
+ void AppendError(const MediaResult& aDecodeError);
+
+ // Implements the "Prepare Append Algorithm". Returns MediaByteBuffer object
+ // on success or nullptr (with aRv set) on error.
+ already_AddRefed<MediaByteBuffer> PrepareAppend(const uint8_t* aData,
+ uint32_t aLength,
+ ErrorResult& aRv);
+
+ void AppendDataCompletedWithSuccess(SourceBufferTask::AppendBufferResult aResult);
+ void AppendDataErrored(const MediaResult& aError);
+
+ RefPtr<MediaSource> mMediaSource;
+
+ RefPtr<TrackBuffersManager> mTrackBuffersManager;
+ SourceBufferAttributes mCurrentAttributes;
+
+ bool mUpdating;
+
+ mozilla::Atomic<bool> mActive;
+
+ MozPromiseRequestHolder<SourceBufferTask::AppendPromise> mPendingAppend;
+ MozPromiseRequestHolder<SourceBufferTask::RangeRemovalPromise> mPendingRemoval;
+ const nsCString mType;
+
+ RefPtr<TimeRanges> mBuffered;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_SourceBuffer_h_ */
diff --git a/dom/media/mediasource/SourceBufferAttributes.h b/dom/media/mediasource/SourceBufferAttributes.h
new file mode 100644
index 000000000..0af80a4fe
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferAttributes.h
@@ -0,0 +1,157 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_SourceBufferAttributes_h_
+#define mozilla_SourceBufferAttributes_h_
+
+#include "TimeUnits.h"
+#include "mozilla/dom/SourceBufferBinding.h"
+#include "mozilla/Maybe.h"
+
+namespace mozilla {
+
+class SourceBufferAttributes {
+public:
+
+ // Current state as per Segment Parser Loop Algorithm
+ // http://w3c.github.io/media-source/index.html#sourcebuffer-segment-parser-loop
+ enum class AppendState
+ {
+ WAITING_FOR_SEGMENT,
+ PARSING_INIT_SEGMENT,
+ PARSING_MEDIA_SEGMENT,
+ };
+
+ explicit SourceBufferAttributes(bool aGenerateTimestamp)
+ : mGenerateTimestamps(aGenerateTimestamp)
+ , mAppendWindowStart(0)
+ , mAppendWindowEnd(PositiveInfinity<double>())
+ , mAppendMode(dom::SourceBufferAppendMode::Segments)
+ , mApparentTimestampOffset(0)
+ , mAppendState(AppendState::WAITING_FOR_SEGMENT)
+ {}
+
+ SourceBufferAttributes(const SourceBufferAttributes& aOther) = default;
+
+ double GetAppendWindowStart() const
+ {
+ return mAppendWindowStart;
+ }
+
+ double GetAppendWindowEnd() const
+ {
+ return mAppendWindowEnd;
+ }
+
+ void SetAppendWindowStart(double aWindowStart)
+ {
+ mAppendWindowStart = aWindowStart;
+ }
+
+ void SetAppendWindowEnd(double aWindowEnd)
+ {
+ mAppendWindowEnd = aWindowEnd;
+ }
+
+ double GetApparentTimestampOffset() const
+ {
+ return mApparentTimestampOffset;
+ }
+
+ void SetApparentTimestampOffset(double aTimestampOffset)
+ {
+ mApparentTimestampOffset = aTimestampOffset;
+ mTimestampOffset = media::TimeUnit::FromSeconds(aTimestampOffset);
+ }
+
+ media::TimeUnit GetTimestampOffset() const
+ {
+ return mTimestampOffset;
+ }
+
+ void SetTimestampOffset(const media::TimeUnit& aTimestampOffset)
+ {
+ mTimestampOffset = aTimestampOffset;
+ mApparentTimestampOffset = aTimestampOffset.ToSeconds();
+ }
+
+ dom::SourceBufferAppendMode GetAppendMode() const
+ {
+ return mAppendMode;
+ }
+
+ void SetAppendMode(dom::SourceBufferAppendMode aAppendMode)
+ {
+ mAppendMode = aAppendMode;
+ }
+
+ void SetGroupStartTimestamp(const media::TimeUnit& aGroupStartTimestamp)
+ {
+ mGroupStartTimestamp = Some(aGroupStartTimestamp);
+ }
+
+ media::TimeUnit GetGroupStartTimestamp() const
+ {
+ return mGroupStartTimestamp.ref();
+ }
+
+ bool HaveGroupStartTimestamp() const
+ {
+ return mGroupStartTimestamp.isSome();
+ }
+
+ void ResetGroupStartTimestamp()
+ {
+ mGroupStartTimestamp.reset();
+ }
+
+ void RestartGroupStartTimestamp()
+ {
+ mGroupStartTimestamp = Some(mGroupEndTimestamp);
+ }
+
+ media::TimeUnit GetGroupEndTimestamp() const
+ {
+ return mGroupEndTimestamp;
+ }
+
+ void SetGroupEndTimestamp(const media::TimeUnit& aGroupEndTimestamp)
+ {
+ mGroupEndTimestamp = aGroupEndTimestamp;
+ }
+
+ AppendState GetAppendState() const
+ {
+ return mAppendState;
+ }
+
+ void SetAppendState(AppendState aState)
+ {
+ mAppendState = aState;
+ }
+
+ // mGenerateTimestamp isn't mutable once the source buffer has been constructed
+ bool mGenerateTimestamps;
+
+ SourceBufferAttributes& operator=(const SourceBufferAttributes& aOther) = default;
+
+private:
+ SourceBufferAttributes() = delete;
+
+ double mAppendWindowStart;
+ double mAppendWindowEnd;
+ dom::SourceBufferAppendMode mAppendMode;
+ double mApparentTimestampOffset;
+ media::TimeUnit mTimestampOffset;
+ Maybe<media::TimeUnit> mGroupStartTimestamp;
+ media::TimeUnit mGroupEndTimestamp;
+ // The current append state as per https://w3c.github.io/media-source/#sourcebuffer-append-state
+ AppendState mAppendState;
+};
+
+} // end namespace mozilla
+
+#endif /* mozilla_SourceBufferAttributes_h_ */
diff --git a/dom/media/mediasource/SourceBufferList.cpp b/dom/media/mediasource/SourceBufferList.cpp
new file mode 100644
index 000000000..cf7a7ed9d
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferList.cpp
@@ -0,0 +1,225 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceBufferList.h"
+
+#include "AsyncEventRunner.h"
+#include "mozilla/ErrorResult.h"
+#include "mozilla/dom/SourceBufferListBinding.h"
+#include "mozilla/mozalloc.h"
+#include "nsCOMPtr.h"
+#include "nsIRunnable.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "mozilla/Logging.h"
+
+extern mozilla::LogModule* GetMediaSourceLog();
+extern mozilla::LogModule* GetMediaSourceAPILog();
+
+#define MSE_API(arg, ...) MOZ_LOG(GetMediaSourceAPILog(), mozilla::LogLevel::Debug, ("SourceBufferList(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+#define MSE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, ("SourceBufferList(%p)::%s: " arg, this, __func__, ##__VA_ARGS__))
+
+struct JSContext;
+class JSObject;
+
+namespace mozilla {
+
+namespace dom {
+
+SourceBufferList::~SourceBufferList()
+{
+}
+
+SourceBuffer*
+SourceBufferList::IndexedGetter(uint32_t aIndex, bool& aFound)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ aFound = aIndex < mSourceBuffers.Length();
+
+ if (!aFound) {
+ return nullptr;
+ }
+ return mSourceBuffers[aIndex];
+}
+
+uint32_t
+SourceBufferList::Length()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSourceBuffers.Length();
+}
+
+void
+SourceBufferList::Append(SourceBuffer* aSourceBuffer)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers.AppendElement(aSourceBuffer);
+ QueueAsyncSimpleEvent("addsourcebuffer");
+}
+
+void
+SourceBufferList::AppendSimple(SourceBuffer* aSourceBuffer)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers.AppendElement(aSourceBuffer);
+}
+
+void
+SourceBufferList::Remove(SourceBuffer* aSourceBuffer)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ALWAYS_TRUE(mSourceBuffers.RemoveElement(aSourceBuffer));
+ aSourceBuffer->Detach();
+ QueueAsyncSimpleEvent("removesourcebuffer");
+}
+
+bool
+SourceBufferList::Contains(SourceBuffer* aSourceBuffer)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSourceBuffers.Contains(aSourceBuffer);
+}
+
+void
+SourceBufferList::Clear()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ mSourceBuffers[i]->Detach();
+ }
+ mSourceBuffers.Clear();
+ QueueAsyncSimpleEvent("removesourcebuffer");
+}
+
+void
+SourceBufferList::ClearSimple()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ mSourceBuffers.Clear();
+}
+
+bool
+SourceBufferList::IsEmpty()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ return mSourceBuffers.IsEmpty();
+}
+
+bool
+SourceBufferList::AnyUpdating()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ if (mSourceBuffers[i]->Updating()) {
+ return true;
+ }
+ }
+ return false;
+}
+
+void
+SourceBufferList::RangeRemoval(double aStart, double aEnd)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("RangeRemoval(aStart=%f, aEnd=%f)", aStart, aEnd);
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ mSourceBuffers[i]->RangeRemoval(aStart, aEnd);
+ }
+}
+
+void
+SourceBufferList::Ended()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ mSourceBuffers[i]->Ended();
+ }
+}
+
+double
+SourceBufferList::GetHighestBufferedEndTime()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ double highestEndTime = 0;
+ for (uint32_t i = 0; i < mSourceBuffers.Length(); ++i) {
+ highestEndTime = std::max(highestEndTime, mSourceBuffers[i]->GetBufferedEnd());
+ }
+ return highestEndTime;
+}
+
+void
+SourceBufferList::DispatchSimpleEvent(const char* aName)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_API("Dispatch event '%s'", aName);
+ DispatchTrustedEvent(NS_ConvertUTF8toUTF16(aName));
+}
+
+void
+SourceBufferList::QueueAsyncSimpleEvent(const char* aName)
+{
+ MSE_DEBUG("Queue event '%s'", aName);
+ nsCOMPtr<nsIRunnable> event = new AsyncEventRunner<SourceBufferList>(this, aName);
+ NS_DispatchToMainThread(event);
+}
+
+SourceBufferList::SourceBufferList(MediaSource* aMediaSource)
+ : DOMEventTargetHelper(aMediaSource->GetParentObject())
+ , mMediaSource(aMediaSource)
+{
+ MOZ_ASSERT(aMediaSource);
+}
+
+MediaSource*
+SourceBufferList::GetParentObject() const
+{
+ return mMediaSource;
+}
+
+double
+SourceBufferList::HighestStartTime()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ double highestStartTime = 0;
+ for (auto& sourceBuffer : mSourceBuffers) {
+ highestStartTime =
+ std::max(sourceBuffer->HighestStartTime(), highestStartTime);
+ }
+ return highestStartTime;
+}
+
+double
+SourceBufferList::HighestEndTime()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ double highestEndTime = 0;
+ for (auto& sourceBuffer : mSourceBuffers) {
+ highestEndTime =
+ std::max(sourceBuffer->HighestEndTime(), highestEndTime);
+ }
+ return highestEndTime;
+}
+
+JSObject*
+SourceBufferList::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
+{
+ return SourceBufferListBinding::Wrap(aCx, this, aGivenProto);
+}
+
+NS_IMPL_CYCLE_COLLECTION_INHERITED(SourceBufferList, DOMEventTargetHelper,
+ mMediaSource, mSourceBuffers)
+
+NS_IMPL_ADDREF_INHERITED(SourceBufferList, DOMEventTargetHelper)
+NS_IMPL_RELEASE_INHERITED(SourceBufferList, DOMEventTargetHelper)
+
+NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(SourceBufferList)
+NS_INTERFACE_MAP_END_INHERITING(DOMEventTargetHelper)
+
+#undef MSE_API
+#undef MSE_DEBUG
+} // namespace dom
+
+} // namespace mozilla
diff --git a/dom/media/mediasource/SourceBufferList.h b/dom/media/mediasource/SourceBufferList.h
new file mode 100644
index 000000000..03dc5c80b
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferList.h
@@ -0,0 +1,106 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_SourceBufferList_h_
+#define mozilla_dom_SourceBufferList_h_
+
+#include "SourceBuffer.h"
+#include "js/RootingAPI.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/DOMEventTargetHelper.h"
+#include "nsCycleCollectionNoteChild.h"
+#include "nsCycleCollectionParticipant.h"
+#include "nsISupports.h"
+#include "nsTArray.h"
+
+struct JSContext;
+class JSObject;
+
+namespace mozilla {
+
+template <typename T> class AsyncEventRunner;
+
+namespace dom {
+
+class MediaSource;
+
+class SourceBufferList final : public DOMEventTargetHelper
+{
+public:
+ /** WebIDL Methods. */
+ SourceBuffer* IndexedGetter(uint32_t aIndex, bool& aFound);
+
+ uint32_t Length();
+
+ IMPL_EVENT_HANDLER(addsourcebuffer);
+ IMPL_EVENT_HANDLER(removesourcebuffer);
+
+ /** End WebIDL methods. */
+
+ NS_DECL_ISUPPORTS_INHERITED
+ NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(SourceBufferList,
+ DOMEventTargetHelper)
+
+ explicit SourceBufferList(MediaSource* aMediaSource);
+
+ MediaSource* GetParentObject() const;
+
+ JSObject* WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) override;
+
+ // Append a SourceBuffer and fire "addsourcebuffer" at the list.
+ void Append(SourceBuffer* aSourceBuffer);
+
+ // Remove a SourceBuffer and fire "removesourcebuffer" at the list.
+ void Remove(SourceBuffer* aSourceBuffer);
+
+ // Returns true if aSourceBuffer is present in the list.
+ bool Contains(SourceBuffer* aSourceBuffer);
+
+ // Remove all SourceBuffers and fire a single "removesourcebuffer" at the list.
+ void Clear();
+
+ // True if list has zero entries.
+ bool IsEmpty();
+
+ // Returns true if updating is true on any SourceBuffers in the list.
+ bool AnyUpdating();
+
+ // Runs the range removal steps from the MSE specification on each SourceBuffer.
+ void RangeRemoval(double aStart, double aEnd);
+
+ // Mark all SourceBuffers input buffers as ended.
+ void Ended();
+
+ // Returns the highest end time of any of the Sourcebuffers.
+ double GetHighestBufferedEndTime();
+
+ // Append a SourceBuffer to the list. No event is fired.
+ void AppendSimple(SourceBuffer* aSourceBuffer);
+
+ // Remove all SourceBuffers from mSourceBuffers.
+ // No event is fired and no action is performed on the sourcebuffers.
+ void ClearSimple();
+
+ double HighestStartTime();
+ double HighestEndTime();
+
+private:
+ ~SourceBufferList();
+
+ friend class AsyncEventRunner<SourceBufferList>;
+ void DispatchSimpleEvent(const char* aName);
+ void QueueAsyncSimpleEvent(const char* aName);
+
+ RefPtr<MediaSource> mMediaSource;
+ nsTArray<RefPtr<SourceBuffer> > mSourceBuffers;
+};
+
+} // namespace dom
+
+} // namespace mozilla
+
+#endif /* mozilla_dom_SourceBufferList_h_ */
diff --git a/dom/media/mediasource/SourceBufferResource.cpp b/dom/media/mediasource/SourceBufferResource.cpp
new file mode 100644
index 000000000..bb11072ae
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferResource.cpp
@@ -0,0 +1,185 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "SourceBufferResource.h"
+
+#include <algorithm>
+
+#include "nsISeekableStream.h"
+#include "nsISupports.h"
+#include "mozilla/Logging.h"
+#include "MediaData.h"
+
+mozilla::LogModule* GetSourceBufferResourceLog()
+{
+ static mozilla::LazyLogModule sLogModule("SourceBufferResource");
+ return sLogModule;
+}
+
+#define SBR_DEBUG(arg, ...) MOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Debug, ("SourceBufferResource(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+#define SBR_DEBUGV(arg, ...) MOZ_LOG(GetSourceBufferResourceLog(), mozilla::LogLevel::Verbose, ("SourceBufferResource(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+nsresult
+SourceBufferResource::Close()
+{
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ SBR_DEBUG("Close");
+ //MOZ_ASSERT(!mClosed);
+ mClosed = true;
+ mon.NotifyAll();
+ return NS_OK;
+}
+
+nsresult
+SourceBufferResource::ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes)
+{
+ SBR_DEBUG("ReadAt(aOffset=%lld, aBuffer=%p, aCount=%u, aBytes=%p)",
+ aOffset, aBytes, aCount, aBytes);
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ return ReadAtInternal(aOffset, aBuffer, aCount, aBytes, /* aMayBlock = */ true);
+}
+
+nsresult
+SourceBufferResource::ReadAtInternal(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes,
+ bool aMayBlock)
+{
+ mMonitor.AssertCurrentThreadIn();
+
+ MOZ_ASSERT_IF(!aMayBlock, aBytes);
+
+ if (mClosed ||
+ aOffset < 0 ||
+ uint64_t(aOffset) < mInputBuffer.GetOffset() ||
+ aOffset > GetLength()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ while (aMayBlock &&
+ !mEnded &&
+ aOffset + aCount > GetLength()) {
+ SBR_DEBUGV("waiting for data");
+ mMonitor.Wait();
+ // The callers of this function should have checked this, but it's
+ // possible that we had an eviction while waiting on the monitor.
+ if (uint64_t(aOffset) < mInputBuffer.GetOffset()) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ uint32_t available = GetLength() - aOffset;
+ uint32_t count = std::min(aCount, available);
+
+ // Keep the position of the last read to have Tell() approximately give us
+ // the position we're up to in the stream.
+ mOffset = aOffset + count;
+
+ SBR_DEBUGV("offset=%llu GetLength()=%u available=%u count=%u mEnded=%d",
+ aOffset, GetLength(), available, count, mEnded);
+ if (available == 0) {
+ SBR_DEBUGV("reached EOF");
+ *aBytes = 0;
+ return NS_OK;
+ }
+
+ mInputBuffer.CopyData(aOffset, count, aBuffer);
+ *aBytes = count;
+
+ return NS_OK;
+}
+
+nsresult
+SourceBufferResource::ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount)
+{
+ SBR_DEBUG("ReadFromCache(aBuffer=%p, aOffset=%lld, aCount=%u)",
+ aBuffer, aOffset, aCount);
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ uint32_t bytesRead;
+ nsresult rv = ReadAtInternal(aOffset, aBuffer, aCount, &bytesRead, /* aMayBlock = */ false);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ // ReadFromCache return failure if not all the data is cached.
+ return bytesRead == aCount ? NS_OK : NS_ERROR_FAILURE;
+}
+
+uint32_t
+SourceBufferResource::EvictData(uint64_t aPlaybackOffset, int64_t aThreshold,
+ ErrorResult& aRv)
+{
+ SBR_DEBUG("EvictData(aPlaybackOffset=%llu,"
+ "aThreshold=%u)", aPlaybackOffset, aThreshold);
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ uint32_t result = mInputBuffer.Evict(aPlaybackOffset, aThreshold, aRv);
+ if (result > 0) {
+ // Wake up any waiting threads in case a ReadInternal call
+ // is now invalid.
+ mon.NotifyAll();
+ }
+ return result;
+}
+
+void
+SourceBufferResource::EvictBefore(uint64_t aOffset, ErrorResult& aRv)
+{
+ SBR_DEBUG("EvictBefore(aOffset=%llu)", aOffset);
+ ReentrantMonitorAutoEnter mon(mMonitor);
+
+ mInputBuffer.EvictBefore(aOffset, aRv);
+
+ // Wake up any waiting threads in case a ReadInternal call
+ // is now invalid.
+ mon.NotifyAll();
+}
+
+uint32_t
+SourceBufferResource::EvictAll()
+{
+ SBR_DEBUG("EvictAll()");
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ return mInputBuffer.EvictAll();
+}
+
+void
+SourceBufferResource::AppendData(MediaByteBuffer* aData)
+{
+ SBR_DEBUG("AppendData(aData=%p, aLength=%u)",
+ aData->Elements(), aData->Length());
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ mInputBuffer.AppendItem(aData);
+ mEnded = false;
+ mon.NotifyAll();
+}
+
+void
+SourceBufferResource::Ended()
+{
+ SBR_DEBUG("");
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ mEnded = true;
+ mon.NotifyAll();
+}
+
+SourceBufferResource::~SourceBufferResource()
+{
+ SBR_DEBUG("");
+ MOZ_COUNT_DTOR(SourceBufferResource);
+}
+
+SourceBufferResource::SourceBufferResource(const nsACString& aType)
+ : mType(aType)
+ , mMonitor("mozilla::SourceBufferResource::mMonitor")
+ , mOffset(0)
+ , mClosed(false)
+ , mEnded(false)
+{
+ SBR_DEBUG("");
+ MOZ_COUNT_CTOR(SourceBufferResource);
+}
+
+#undef SBR_DEBUG
+#undef SBR_DEBUGV
+} // namespace mozilla
diff --git a/dom/media/mediasource/SourceBufferResource.h b/dom/media/mediasource/SourceBufferResource.h
new file mode 100644
index 000000000..26ed74602
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferResource.h
@@ -0,0 +1,161 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_SOURCEBUFFERRESOURCE_H_
+#define MOZILLA_SOURCEBUFFERRESOURCE_H_
+
+#include "MediaCache.h"
+#include "MediaResource.h"
+#include "ResourceQueue.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/ReentrantMonitor.h"
+#include "nsCOMPtr.h"
+#include "nsError.h"
+#include "nsIPrincipal.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nscore.h"
+#include "mozilla/Logging.h"
+
+#define UNIMPLEMENTED() { /* Logging this is too spammy to do by default */ }
+
+class nsIStreamListener;
+
+namespace mozilla {
+
+class MediaDecoder;
+class MediaByteBuffer;
+
+namespace dom {
+
+class SourceBuffer;
+
+} // namespace dom
+
+class SourceBufferResource final : public MediaResource
+{
+public:
+ explicit SourceBufferResource(const nsACString& aType);
+ nsresult Close() override;
+ void Suspend(bool aCloseImmediately) override { UNIMPLEMENTED(); }
+ void Resume() override { UNIMPLEMENTED(); }
+ already_AddRefed<nsIPrincipal> GetCurrentPrincipal() override { UNIMPLEMENTED(); return nullptr; }
+ already_AddRefed<MediaResource> CloneData(MediaResourceCallback*) override { UNIMPLEMENTED(); return nullptr; }
+ void SetReadMode(MediaCacheStream::ReadMode aMode) override { UNIMPLEMENTED(); }
+ void SetPlaybackRate(uint32_t aBytesPerSecond) override { UNIMPLEMENTED(); }
+ nsresult ReadAt(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes) override;
+ int64_t Tell() override { return mOffset; }
+ void Pin() override { UNIMPLEMENTED(); }
+ void Unpin() override { UNIMPLEMENTED(); }
+ double GetDownloadRate(bool* aIsReliable) override { UNIMPLEMENTED(); *aIsReliable = false; return 0; }
+ int64_t GetLength() override { return mInputBuffer.GetLength(); }
+ int64_t GetNextCachedData(int64_t aOffset) override {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ MOZ_ASSERT(aOffset >= 0);
+ if (uint64_t(aOffset) < mInputBuffer.GetOffset()) {
+ return mInputBuffer.GetOffset();
+ } else if (aOffset == GetLength()) {
+ return -1;
+ }
+ return aOffset;
+ }
+ int64_t GetCachedDataEnd(int64_t aOffset) override { UNIMPLEMENTED(); return -1; }
+ bool IsDataCachedToEndOfResource(int64_t aOffset) override { return false; }
+ bool IsSuspendedByCache() override { UNIMPLEMENTED(); return false; }
+ bool IsSuspended() override { UNIMPLEMENTED(); return false; }
+ nsresult ReadFromCache(char* aBuffer, int64_t aOffset, uint32_t aCount) override;
+ bool IsTransportSeekable() override { UNIMPLEMENTED(); return true; }
+ nsresult Open(nsIStreamListener** aStreamListener) override { UNIMPLEMENTED(); return NS_ERROR_FAILURE; }
+
+ nsresult GetCachedRanges(MediaByteRangeSet& aRanges) override
+ {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ if (mInputBuffer.GetLength()) {
+ aRanges += MediaByteRange(mInputBuffer.GetOffset(),
+ mInputBuffer.GetLength());
+ }
+ return NS_OK;
+ }
+
+ const nsCString& GetContentType() const override { return mType; }
+
+ size_t SizeOfExcludingThis(MallocSizeOf aMallocSizeOf) const override
+ {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+
+ size_t size = MediaResource::SizeOfExcludingThis(aMallocSizeOf);
+ size += mType.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ size += mInputBuffer.SizeOfExcludingThis(aMallocSizeOf);
+
+ return size;
+ }
+
+ size_t SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const override
+ {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+ }
+
+ bool IsExpectingMoreData() override
+ {
+ return false;
+ }
+
+ // Used by SourceBuffer.
+ void AppendData(MediaByteBuffer* aData);
+ void Ended();
+ bool IsEnded()
+ {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ return mEnded;
+ }
+ // Remove data from resource if it holds more than the threshold reduced by
+ // the given number of bytes. Returns amount evicted.
+ uint32_t EvictData(uint64_t aPlaybackOffset, int64_t aThresholdReduct,
+ ErrorResult& aRv);
+
+ // Remove data from resource before the given offset.
+ void EvictBefore(uint64_t aOffset, ErrorResult& aRv);
+
+ // Remove all data from the resource
+ uint32_t EvictAll();
+
+ // Returns the amount of data currently retained by this resource.
+ int64_t GetSize() {
+ ReentrantMonitorAutoEnter mon(mMonitor);
+ return mInputBuffer.GetLength() - mInputBuffer.GetOffset();
+ }
+
+#if defined(DEBUG)
+ void Dump(const char* aPath) {
+ mInputBuffer.Dump(aPath);
+ }
+#endif
+
+private:
+ virtual ~SourceBufferResource();
+ nsresult ReadAtInternal(int64_t aOffset, char* aBuffer, uint32_t aCount, uint32_t* aBytes, bool aMayBlock);
+
+ const nsCString mType;
+
+ // Provides synchronization between SourceBuffers and InputAdapters.
+ // Protects all of the member variables below. Read() will await a
+ // Notify() (from Seek, AppendData, Ended, or Close) when insufficient
+ // data is available in mData.
+ mutable ReentrantMonitor mMonitor;
+
+ // The buffer holding resource data.
+ ResourceQueue mInputBuffer;
+
+ uint64_t mOffset;
+ bool mClosed;
+ bool mEnded;
+};
+
+} // namespace mozilla
+
+#undef UNIMPLEMENTED
+
+#endif /* MOZILLA_SOURCEBUFFERRESOURCE_H_ */
diff --git a/dom/media/mediasource/SourceBufferTask.h b/dom/media/mediasource/SourceBufferTask.h
new file mode 100644
index 000000000..868b60859
--- /dev/null
+++ b/dom/media/mediasource/SourceBufferTask.h
@@ -0,0 +1,111 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_SOURCEBUFFERTASK_H_
+#define MOZILLA_SOURCEBUFFERTASK_H_
+
+#include "mozilla/MozPromise.h"
+#include "mozilla/Pair.h"
+#include "mozilla/RefPtr.h"
+#include "SourceBufferAttributes.h"
+#include "TimeUnits.h"
+#include "MediaResult.h"
+
+namespace mozilla {
+
+class SourceBufferTask {
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(SourceBufferTask);
+ enum class Type {
+ AppendBuffer,
+ Abort,
+ Reset,
+ RangeRemoval,
+ EvictData,
+ Detach
+ };
+
+ typedef Pair<bool, SourceBufferAttributes> AppendBufferResult;
+ typedef MozPromise<AppendBufferResult, MediaResult, /* IsExclusive = */ true> AppendPromise;
+ typedef MozPromise<bool, nsresult, /* IsExclusive = */ true> RangeRemovalPromise;
+
+ virtual Type GetType() const = 0;
+
+ template<typename ReturnType>
+ ReturnType* As()
+ {
+ MOZ_ASSERT(this->GetType() == ReturnType::sType);
+ return static_cast<ReturnType*>(this);
+ }
+
+protected:
+ virtual ~SourceBufferTask() {}
+};
+
+class AppendBufferTask : public SourceBufferTask {
+public:
+ AppendBufferTask(MediaByteBuffer* aData,
+ SourceBufferAttributes aAttributes)
+ : mBuffer(aData)
+ , mAttributes(aAttributes)
+ {}
+
+ static const Type sType = Type::AppendBuffer;
+ Type GetType() const override { return Type::AppendBuffer; }
+
+ RefPtr<MediaByteBuffer> mBuffer;
+ SourceBufferAttributes mAttributes;
+ MozPromiseHolder<AppendPromise> mPromise;
+};
+
+class AbortTask : public SourceBufferTask {
+public:
+ static const Type sType = Type::Abort;
+ Type GetType() const override { return Type::Abort; }
+};
+
+class ResetTask : public SourceBufferTask {
+public:
+ static const Type sType = Type::Reset;
+ Type GetType() const override { return Type::Reset; }
+};
+
+class RangeRemovalTask : public SourceBufferTask {
+public:
+ explicit RangeRemovalTask(const media::TimeInterval& aRange)
+ : mRange(aRange)
+ {}
+
+ static const Type sType = Type::RangeRemoval;
+ Type GetType() const override { return Type::RangeRemoval; }
+
+ media::TimeInterval mRange;
+ MozPromiseHolder<RangeRemovalPromise> mPromise;
+};
+
+class EvictDataTask : public SourceBufferTask {
+public:
+ EvictDataTask(const media::TimeUnit& aPlaybackTime, int64_t aSizetoEvict)
+ : mPlaybackTime(aPlaybackTime)
+ , mSizeToEvict(aSizetoEvict)
+ {}
+
+ static const Type sType = Type::EvictData;
+ Type GetType() const override { return Type::EvictData; }
+
+ media::TimeUnit mPlaybackTime;
+ int64_t mSizeToEvict;
+};
+
+class DetachTask : public SourceBufferTask {
+public:
+ static const Type sType = Type::Detach;
+ Type GetType() const override { return Type::Detach; }
+};
+
+} // end mozilla namespace
+
+#endif \ No newline at end of file
diff --git a/dom/media/mediasource/TrackBuffersManager.cpp b/dom/media/mediasource/TrackBuffersManager.cpp
new file mode 100644
index 000000000..4265aed81
--- /dev/null
+++ b/dom/media/mediasource/TrackBuffersManager.cpp
@@ -0,0 +1,2505 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TrackBuffersManager.h"
+#include "ContainerParser.h"
+#include "MediaSourceDemuxer.h"
+#include "MediaSourceUtils.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StateMirroring.h"
+#include "SourceBufferResource.h"
+#include "SourceBuffer.h"
+#include "WebMDemuxer.h"
+#include "SourceBufferTask.h"
+
+#ifdef MOZ_FMP4
+#include "MP4Demuxer.h"
+#endif
+
+#include <limits>
+
+extern mozilla::LogModule* GetMediaSourceLog();
+
+#define MSE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Debug, ("TrackBuffersManager(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+#define MSE_DEBUGV(arg, ...) MOZ_LOG(GetMediaSourceLog(), mozilla::LogLevel::Verbose, ("TrackBuffersManager(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+
+mozilla::LogModule* GetMediaSourceSamplesLog()
+{
+ static mozilla::LazyLogModule sLogModule("MediaSourceSamples");
+ return sLogModule;
+}
+#define SAMPLE_DEBUG(arg, ...) MOZ_LOG(GetMediaSourceSamplesLog(), mozilla::LogLevel::Debug, ("TrackBuffersManager(%p:%s)::%s: " arg, this, mType.get(), __func__, ##__VA_ARGS__))
+
+namespace mozilla {
+
+using dom::SourceBufferAppendMode;
+using media::TimeUnit;
+using media::TimeInterval;
+using media::TimeIntervals;
+typedef SourceBufferTask::AppendBufferResult AppendBufferResult;
+
+static const char*
+AppendStateToStr(SourceBufferAttributes::AppendState aState)
+{
+ switch (aState) {
+ case SourceBufferAttributes::AppendState::WAITING_FOR_SEGMENT:
+ return "WAITING_FOR_SEGMENT";
+ case SourceBufferAttributes::AppendState::PARSING_INIT_SEGMENT:
+ return "PARSING_INIT_SEGMENT";
+ case SourceBufferAttributes::AppendState::PARSING_MEDIA_SEGMENT:
+ return "PARSING_MEDIA_SEGMENT";
+ default:
+ return "IMPOSSIBLE";
+ }
+}
+
+static Atomic<uint32_t> sStreamSourceID(0u);
+
+class DispatchKeyNeededEvent : public Runnable {
+public:
+ DispatchKeyNeededEvent(AbstractMediaDecoder* aDecoder,
+ nsTArray<uint8_t>& aInitData,
+ const nsString& aInitDataType)
+ : mDecoder(aDecoder)
+ , mInitData(aInitData)
+ , mInitDataType(aInitDataType)
+ {
+ }
+ NS_IMETHOD Run() override {
+ // Note: Null check the owner, as the decoder could have been shutdown
+ // since this event was dispatched.
+ MediaDecoderOwner* owner = mDecoder->GetOwner();
+ if (owner) {
+ owner->DispatchEncrypted(mInitData, mInitDataType);
+ }
+ mDecoder = nullptr;
+ return NS_OK;
+ }
+private:
+ RefPtr<AbstractMediaDecoder> mDecoder;
+ nsTArray<uint8_t> mInitData;
+ nsString mInitDataType;
+};
+
+TrackBuffersManager::TrackBuffersManager(MediaSourceDecoder* aParentDecoder,
+ const nsACString& aType)
+ : mInputBuffer(new MediaByteBuffer)
+ , mBufferFull(false)
+ , mFirstInitializationSegmentReceived(false)
+ , mNewMediaSegmentStarted(false)
+ , mActiveTrack(false)
+ , mType(aType)
+ , mParser(ContainerParser::CreateForMIMEType(aType))
+ , mProcessedInput(0)
+ , mTaskQueue(aParentDecoder->GetDemuxer()->GetTaskQueue())
+ , mParentDecoder(new nsMainThreadPtrHolder<MediaSourceDecoder>(aParentDecoder, false /* strict */))
+ , mEnded(false)
+ , mVideoEvictionThreshold(Preferences::GetUint("media.mediasource.eviction_threshold.video",
+ 100 * 1024 * 1024))
+ , mAudioEvictionThreshold(Preferences::GetUint("media.mediasource.eviction_threshold.audio",
+ 20 * 1024 * 1024))
+ , mEvictionState(EvictionState::NO_EVICTION_NEEDED)
+ , mMonitor("TrackBuffersManager")
+{
+ MOZ_ASSERT(NS_IsMainThread(), "Must be instanciated on the main thread");
+}
+
+TrackBuffersManager::~TrackBuffersManager()
+{
+ ShutdownDemuxers();
+}
+
+RefPtr<TrackBuffersManager::AppendPromise>
+TrackBuffersManager::AppendData(MediaByteBuffer* aData,
+ const SourceBufferAttributes& aAttributes)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("Appending %lld bytes", aData->Length());
+
+ mEnded = false;
+
+ RefPtr<MediaByteBuffer> buffer = aData;
+
+ return InvokeAsync(GetTaskQueue(), this,
+ __func__, &TrackBuffersManager::DoAppendData,
+ buffer, aAttributes);
+}
+
+RefPtr<TrackBuffersManager::AppendPromise>
+TrackBuffersManager::DoAppendData(RefPtr<MediaByteBuffer> aData,
+ SourceBufferAttributes aAttributes)
+{
+ RefPtr<AppendBufferTask> task = new AppendBufferTask(aData, aAttributes);
+ RefPtr<AppendPromise> p = task->mPromise.Ensure(__func__);
+ QueueTask(task);
+
+ return p;
+}
+
+void
+TrackBuffersManager::QueueTask(SourceBufferTask* aTask)
+{
+ if (!OnTaskQueue()) {
+ GetTaskQueue()->Dispatch(NewRunnableMethod<RefPtr<SourceBufferTask>>(
+ this, &TrackBuffersManager::QueueTask, aTask));
+ return;
+ }
+ MOZ_ASSERT(OnTaskQueue());
+ mQueue.Push(aTask);
+ ProcessTasks();
+}
+
+void
+TrackBuffersManager::ProcessTasks()
+{
+ MOZ_ASSERT(OnTaskQueue());
+ typedef SourceBufferTask::Type Type;
+
+ if (mCurrentTask) {
+ // Already have a task pending. ProcessTask will be scheduled once the
+ // current task complete.
+ return;
+ }
+ RefPtr<SourceBufferTask> task = mQueue.Pop();
+ if (!task) {
+ // nothing to do.
+ return;
+ }
+ switch (task->GetType()) {
+ case Type::AppendBuffer:
+ mCurrentTask = task;
+ if (!mInputBuffer) {
+ mInputBuffer = task->As<AppendBufferTask>()->mBuffer;
+ } else if (!mInputBuffer->AppendElements(*task->As<AppendBufferTask>()->mBuffer, fallible)) {
+ RejectAppend(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+ mSourceBufferAttributes =
+ MakeUnique<SourceBufferAttributes>(task->As<AppendBufferTask>()->mAttributes);
+ mAppendWindow =
+ TimeInterval(TimeUnit::FromSeconds(mSourceBufferAttributes->GetAppendWindowStart()),
+ TimeUnit::FromSeconds(mSourceBufferAttributes->GetAppendWindowEnd()));
+ ScheduleSegmentParserLoop();
+ break;
+ case Type::RangeRemoval:
+ {
+ bool rv = CodedFrameRemoval(task->As<RangeRemovalTask>()->mRange);
+ task->As<RangeRemovalTask>()->mPromise.Resolve(rv, __func__);
+ break;
+ }
+ case Type::EvictData:
+ DoEvictData(task->As<EvictDataTask>()->mPlaybackTime,
+ task->As<EvictDataTask>()->mSizeToEvict);
+ break;
+ case Type::Abort:
+ // not handled yet, and probably never.
+ break;
+ case Type::Reset:
+ CompleteResetParserState();
+ break;
+ case Type::Detach:
+ mTaskQueue = nullptr;
+ MOZ_DIAGNOSTIC_ASSERT(mQueue.Length() == 0,
+ "Detach task must be the last");
+ return;
+ default:
+ NS_WARNING("Invalid Task");
+ }
+ GetTaskQueue()->Dispatch(NewRunnableMethod(this, &TrackBuffersManager::ProcessTasks));
+}
+
+// The MSE spec requires that we abort the current SegmentParserLoop
+// which is then followed by a call to ResetParserState.
+// However due to our asynchronous design this causes inherent difficulties.
+// As the spec behaviour is non deterministic anyway, we instead process all
+// pending frames found in the input buffer.
+void
+TrackBuffersManager::AbortAppendData()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("");
+
+ QueueTask(new AbortTask());
+}
+
+void
+TrackBuffersManager::ResetParserState(SourceBufferAttributes& aAttributes)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("");
+
+ // Spec states:
+ // 1. If the append state equals PARSING_MEDIA_SEGMENT and the input buffer contains some complete coded frames, then run the coded frame processing algorithm until all of these complete coded frames have been processed.
+ // However, we will wait until all coded frames have been processed regardless
+ // of the value of append state.
+ QueueTask(new ResetTask());
+
+ // ResetParserState has some synchronous steps that much be performed now.
+ // The remaining steps will be performed once the ResetTask gets executed.
+
+ // 6. If the mode attribute equals "sequence", then set the group start timestamp to the group end timestamp
+ if (aAttributes.GetAppendMode() == SourceBufferAppendMode::Sequence) {
+ aAttributes.SetGroupStartTimestamp(aAttributes.GetGroupEndTimestamp());
+ }
+ // 8. Set append state to WAITING_FOR_SEGMENT.
+ aAttributes.SetAppendState(AppendState::WAITING_FOR_SEGMENT);
+}
+
+RefPtr<TrackBuffersManager::RangeRemovalPromise>
+TrackBuffersManager::RangeRemoval(TimeUnit aStart, TimeUnit aEnd)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("From %.2f to %.2f", aStart.ToSeconds(), aEnd.ToSeconds());
+
+ mEnded = false;
+
+ return InvokeAsync(GetTaskQueue(), this, __func__,
+ &TrackBuffersManager::CodedFrameRemovalWithPromise,
+ TimeInterval(aStart, aEnd));
+}
+
+TrackBuffersManager::EvictDataResult
+TrackBuffersManager::EvictData(const TimeUnit& aPlaybackTime, int64_t aSize)
+{
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aSize > EvictionThreshold()) {
+ // We're adding more data than we can hold.
+ return EvictDataResult::BUFFER_FULL;
+ }
+ const int64_t toEvict = GetSize() + aSize - EvictionThreshold();
+
+ const uint32_t canEvict =
+ Evictable(HasVideo() ? TrackInfo::kVideoTrack : TrackInfo::kAudioTrack);
+
+ MSE_DEBUG("currentTime=%lld buffered=%lldkB, eviction threshold=%ukB, "
+ "evict=%lldkB canevict=%ukB",
+ aPlaybackTime.ToMicroseconds(), GetSize() / 1024,
+ EvictionThreshold() / 1024, toEvict / 1024, canEvict / 1024);
+
+ if (toEvict <= 0) {
+ mEvictionState = EvictionState::NO_EVICTION_NEEDED;
+ return EvictDataResult::NO_DATA_EVICTED;
+ }
+
+ EvictDataResult result;
+
+ if (mBufferFull && mEvictionState == EvictionState::EVICTION_COMPLETED &&
+ canEvict < uint32_t(toEvict)) {
+ // Our buffer is currently full. We will make another eviction attempt.
+ // However, the current appendBuffer will fail as we can't know ahead of
+ // time if the eviction will later succeed.
+ result = EvictDataResult::BUFFER_FULL;
+ } else {
+ mEvictionState = EvictionState::EVICTION_NEEDED;
+ result = EvictDataResult::NO_DATA_EVICTED;
+ }
+ MSE_DEBUG(
+ "Reached our size limit, schedule eviction of %lld bytes (%s)", toEvict,
+ result == EvictDataResult::BUFFER_FULL ? "buffer full" : "no data evicted");
+ QueueTask(new EvictDataTask(aPlaybackTime, toEvict));
+
+ return result;
+}
+
+TimeIntervals
+TrackBuffersManager::Buffered() const
+{
+ MSE_DEBUG("");
+
+ // http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
+
+ MonitorAutoLock mon(mMonitor);
+ nsTArray<const TimeIntervals*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoBufferedRanges);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioBufferedRanges);
+ }
+
+ // 2. Let highest end time be the largest track buffer ranges end time across all the track buffers managed by this SourceBuffer object.
+ TimeUnit highestEndTime = HighestEndTime(tracks);
+
+ // 3. Let intersection ranges equal a TimeRange object containing a single range from 0 to highest end time.
+ TimeIntervals intersection{TimeInterval(TimeUnit::FromSeconds(0), highestEndTime)};
+
+ // 4. For each track buffer managed by this SourceBuffer, run the following steps:
+ // 1. Let track ranges equal the track buffer ranges for the current track buffer.
+ for (const TimeIntervals* trackRanges : tracks) {
+ // 2. If readyState is "ended", then set the end time on the last range in track ranges to highest end time.
+ // 3. Let new intersection ranges equal the intersection between the intersection ranges and the track ranges.
+ if (mEnded) {
+ TimeIntervals tR = *trackRanges;
+ tR.Add(TimeInterval(tR.GetEnd(), highestEndTime));
+ intersection.Intersection(tR);
+ } else {
+ intersection.Intersection(*trackRanges);
+ }
+ }
+ return intersection;
+}
+
+int64_t
+TrackBuffersManager::GetSize() const
+{
+ return mSizeSourceBuffer;
+}
+
+void
+TrackBuffersManager::Ended()
+{
+ mEnded = true;
+}
+
+void
+TrackBuffersManager::Detach()
+{
+ MOZ_ASSERT(NS_IsMainThread());
+ MSE_DEBUG("");
+ QueueTask(new DetachTask());
+}
+
+void
+TrackBuffersManager::CompleteResetParserState()
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MSE_DEBUG("");
+
+ // We shouldn't change mInputDemuxer while a demuxer init/reset request is
+ // being processed. See bug 1239983.
+ MOZ_DIAGNOSTIC_ASSERT(!mDemuxerInitRequest.Exists(), "Previous AppendBuffer didn't complete");
+
+ for (auto& track : GetTracksList()) {
+ // 2. Unset the last decode timestamp on all track buffers.
+ // 3. Unset the last frame duration on all track buffers.
+ // 4. Unset the highest end timestamp on all track buffers.
+ // 5. Set the need random access point flag on all track buffers to true.
+ track->ResetAppendState();
+
+ // if we have been aborted, we may have pending frames that we are going
+ // to discard now.
+ track->mQueuedSamples.Clear();
+ }
+
+ // 7. Remove all bytes from the input buffer.
+ mInputBuffer = nullptr;
+ if (mCurrentInputBuffer) {
+ mCurrentInputBuffer->EvictAll();
+ // The demuxer will be recreated during the next run of SegmentParserLoop.
+ // As such we don't need to notify it that data has been removed.
+ mCurrentInputBuffer = new SourceBufferResource(mType);
+ }
+
+ // We could be left with a demuxer in an unusable state. It needs to be
+ // recreated. We store in the InputBuffer an init segment which will be parsed
+ // during the next Segment Parser Loop and a new demuxer will be created and
+ // initialized.
+ if (mFirstInitializationSegmentReceived) {
+ MOZ_ASSERT(mInitData && mInitData->Length(), "we must have an init segment");
+ // The aim here is really to destroy our current demuxer.
+ CreateDemuxerforMIMEType();
+ // Recreate our input buffer. We can't directly assign the initData buffer
+ // to mInputBuffer as it will get modified in the Segment Parser Loop.
+ mInputBuffer = new MediaByteBuffer;
+ mInputBuffer->AppendElements(*mInitData);
+ }
+ RecreateParser(true);
+}
+
+int64_t
+TrackBuffersManager::EvictionThreshold() const
+{
+ if (HasVideo()) {
+ return mVideoEvictionThreshold;
+ }
+ return mAudioEvictionThreshold;
+}
+
+void
+TrackBuffersManager::DoEvictData(const TimeUnit& aPlaybackTime,
+ int64_t aSizeToEvict)
+{
+ MOZ_ASSERT(OnTaskQueue());
+
+ mEvictionState = EvictionState::EVICTION_COMPLETED;
+
+ // Video is what takes the most space, only evict there if we have video.
+ auto& track = HasVideo() ? mVideoTracks : mAudioTracks;
+ const auto& buffer = track.GetTrackBuffer();
+ // Remove any data we've already played, or before the next sample to be
+ // demuxed whichever is lowest.
+ TimeUnit lowerLimit = std::min(track.mNextSampleTime, aPlaybackTime);
+ uint32_t lastKeyFrameIndex = 0;
+ int64_t toEvict = aSizeToEvict;
+ int64_t partialEvict = 0;
+ for (uint32_t i = 0; i < buffer.Length(); i++) {
+ const auto& frame = buffer[i];
+ if (frame->mKeyframe) {
+ lastKeyFrameIndex = i;
+ toEvict -= partialEvict;
+ if (toEvict < 0) {
+ break;
+ }
+ partialEvict = 0;
+ }
+ if (frame->GetEndTime() >= lowerLimit.ToMicroseconds()) {
+ break;
+ }
+ partialEvict += frame->ComputedSizeOfIncludingThis();
+ }
+
+ const int64_t finalSize = mSizeSourceBuffer - aSizeToEvict;
+
+ if (lastKeyFrameIndex > 0) {
+ MSE_DEBUG("Step1. Evicting %lld bytes prior currentTime",
+ aSizeToEvict - toEvict);
+ CodedFrameRemoval(
+ TimeInterval(TimeUnit::FromMicroseconds(0),
+ TimeUnit::FromMicroseconds(buffer[lastKeyFrameIndex]->mTime - 1)));
+ }
+
+ if (mSizeSourceBuffer <= finalSize) {
+ return;
+ }
+
+ toEvict = mSizeSourceBuffer - finalSize;
+
+ // See if we can evict data into the future.
+ // We do not evict data from the currently used buffered interval.
+
+ TimeUnit currentPosition = std::max(aPlaybackTime, track.mNextSampleTime);
+ TimeIntervals futureBuffered(TimeInterval(currentPosition, TimeUnit::FromInfinity()));
+ futureBuffered.Intersection(track.mBufferedRanges);
+ futureBuffered.SetFuzz(MediaSourceDemuxer::EOS_FUZZ / 2);
+ if (futureBuffered.Length() <= 1) {
+ // We have one continuous segment ahead of us:
+ // nothing further can be evicted.
+ return;
+ }
+
+ // Don't evict before the end of the current segment
+ TimeUnit upperLimit = futureBuffered[0].mEnd;
+ uint32_t evictedFramesStartIndex = buffer.Length();
+ for (int32_t i = buffer.Length() - 1; i >= 0; i--) {
+ const auto& frame = buffer[i];
+ if (frame->mTime <= upperLimit.ToMicroseconds() || toEvict < 0) {
+ // We've reached a frame that shouldn't be evicted -> Evict after it -> i+1.
+ // Or the previous loop reached the eviction threshold -> Evict from it -> i+1.
+ evictedFramesStartIndex = i + 1;
+ break;
+ }
+ toEvict -= frame->ComputedSizeOfIncludingThis();
+ }
+ if (evictedFramesStartIndex < buffer.Length()) {
+ MSE_DEBUG("Step2. Evicting %lld bytes from trailing data",
+ mSizeSourceBuffer - finalSize - toEvict);
+ CodedFrameRemoval(
+ TimeInterval(TimeUnit::FromMicroseconds(buffer[evictedFramesStartIndex]->mTime),
+ TimeUnit::FromInfinity()));
+ }
+}
+
+RefPtr<TrackBuffersManager::RangeRemovalPromise>
+TrackBuffersManager::CodedFrameRemovalWithPromise(TimeInterval aInterval)
+{
+ MOZ_ASSERT(OnTaskQueue());
+
+ RefPtr<RangeRemovalTask> task = new RangeRemovalTask(aInterval);
+ RefPtr<RangeRemovalPromise> p = task->mPromise.Ensure(__func__);
+ QueueTask(task);
+
+ return p;
+}
+
+bool
+TrackBuffersManager::CodedFrameRemoval(TimeInterval aInterval)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MSE_DEBUG("From %.2fs to %.2f",
+ aInterval.mStart.ToSeconds(), aInterval.mEnd.ToSeconds());
+
+#if DEBUG
+ if (HasVideo()) {
+ MSE_DEBUG("before video ranges=%s",
+ DumpTimeRanges(mVideoTracks.mBufferedRanges).get());
+ }
+ if (HasAudio()) {
+ MSE_DEBUG("before audio ranges=%s",
+ DumpTimeRanges(mAudioTracks.mBufferedRanges).get());
+ }
+#endif
+
+ // 1. Let start be the starting presentation timestamp for the removal range.
+ TimeUnit start = aInterval.mStart;
+ // 2. Let end be the end presentation timestamp for the removal range.
+ TimeUnit end = aInterval.mEnd;
+
+ bool dataRemoved = false;
+
+ // 3. For each track buffer in this source buffer, run the following steps:
+ for (auto track : GetTracksList()) {
+ MSE_DEBUGV("Processing %s track", track->mInfo->mMimeType.get());
+ // 1. Let remove end timestamp be the current value of duration
+ // See bug: https://www.w3.org/Bugs/Public/show_bug.cgi?id=28727
+ // At worse we will remove all frames until the end, unless a key frame is
+ // found between the current interval's end and the trackbuffer's end.
+ TimeUnit removeEndTimestamp = track->mBufferedRanges.GetEnd();
+
+ if (start > removeEndTimestamp) {
+ // Nothing to remove.
+ continue;
+ }
+
+ // 2. If this track buffer has a random access point timestamp that is greater than or equal to end,
+ // then update remove end timestamp to that random access point timestamp.
+ if (end < track->mBufferedRanges.GetEnd()) {
+ for (auto& frame : track->GetTrackBuffer()) {
+ if (frame->mKeyframe && frame->mTime >= end.ToMicroseconds()) {
+ removeEndTimestamp = TimeUnit::FromMicroseconds(frame->mTime);
+ break;
+ }
+ }
+ }
+
+ // 3. Remove all media data, from this track buffer, that contain starting
+ // timestamps greater than or equal to start and less than the remove end timestamp.
+ // 4. Remove decoding dependencies of the coded frames removed in the previous step:
+ // Remove all coded frames between the coded frames removed in the previous step and the next random access point after those removed frames.
+ TimeIntervals removedInterval{TimeInterval(start, removeEndTimestamp)};
+ RemoveFrames(removedInterval, *track, 0);
+
+ // 5. If this object is in activeSourceBuffers, the current playback position
+ // is greater than or equal to start and less than the remove end timestamp,
+ // and HTMLMediaElement.readyState is greater than HAVE_METADATA, then set the
+ // HTMLMediaElement.readyState attribute to HAVE_METADATA and stall playback.
+ // This will be done by the MDSM during playback.
+ // TODO properly, so it works even if paused.
+ }
+
+ UpdateBufferedRanges();
+
+ // Update our reported total size.
+ mSizeSourceBuffer = mVideoTracks.mSizeBuffer + mAudioTracks.mSizeBuffer;
+
+ // 4. If buffer full flag equals true and this object is ready to accept more bytes, then set the buffer full flag to false.
+ if (mBufferFull && mSizeSourceBuffer < EvictionThreshold()) {
+ mBufferFull = false;
+ }
+
+ return dataRemoved;
+}
+
+void
+TrackBuffersManager::UpdateBufferedRanges()
+{
+ MonitorAutoLock mon(mMonitor);
+
+ mVideoBufferedRanges = mVideoTracks.mSanitizedBufferedRanges;
+ mAudioBufferedRanges = mAudioTracks.mSanitizedBufferedRanges;
+
+#if DEBUG
+ if (HasVideo()) {
+ MSE_DEBUG("after video ranges=%s",
+ DumpTimeRanges(mVideoTracks.mBufferedRanges).get());
+ }
+ if (HasAudio()) {
+ MSE_DEBUG("after audio ranges=%s",
+ DumpTimeRanges(mAudioTracks.mBufferedRanges).get());
+ }
+#endif
+}
+
+void
+TrackBuffersManager::SegmentParserLoop()
+{
+ MOZ_ASSERT(OnTaskQueue());
+
+ while (true) {
+ // 1. If the input buffer is empty, then jump to the need more data step below.
+ if (!mInputBuffer || mInputBuffer->IsEmpty()) {
+ NeedMoreData();
+ return;
+ }
+ // 2. If the input buffer contains bytes that violate the SourceBuffer
+ // byte stream format specification, then run the append error algorithm with
+ // the decode error parameter set to true and abort this algorithm.
+ // TODO
+
+ // 3. Remove any bytes that the byte stream format specifications say must be
+ // ignored from the start of the input buffer.
+ // We do not remove bytes from our input buffer. Instead we enforce that
+ // our ContainerParser is able to skip over all data that is supposed to be
+ // ignored.
+
+ // 4. If the append state equals WAITING_FOR_SEGMENT, then run the following
+ // steps:
+ if (mSourceBufferAttributes->GetAppendState() == AppendState::WAITING_FOR_SEGMENT) {
+ MediaResult haveInitSegment = mParser->IsInitSegmentPresent(mInputBuffer);
+ if (NS_SUCCEEDED(haveInitSegment)) {
+ SetAppendState(AppendState::PARSING_INIT_SEGMENT);
+ if (mFirstInitializationSegmentReceived) {
+ // This is a new initialization segment. Obsolete the old one.
+ RecreateParser(false);
+ }
+ continue;
+ }
+ MediaResult haveMediaSegment =
+ mParser->IsMediaSegmentPresent(mInputBuffer);
+ if (NS_SUCCEEDED(haveMediaSegment)) {
+ SetAppendState(AppendState::PARSING_MEDIA_SEGMENT);
+ mNewMediaSegmentStarted = true;
+ continue;
+ }
+ // We have neither an init segment nor a media segment.
+ // Check if it was invalid data.
+ if (haveInitSegment != NS_ERROR_NOT_AVAILABLE) {
+ MSE_DEBUG("Found invalid data.");
+ RejectAppend(haveInitSegment, __func__);
+ return;
+ }
+ if (haveMediaSegment != NS_ERROR_NOT_AVAILABLE) {
+ MSE_DEBUG("Found invalid data.");
+ RejectAppend(haveMediaSegment, __func__);
+ return;
+ }
+ MSE_DEBUG("Found incomplete data.");
+ NeedMoreData();
+ return;
+ }
+
+ int64_t start, end;
+ MediaResult newData =
+ mParser->ParseStartAndEndTimestamps(mInputBuffer, start, end);
+ if (!NS_SUCCEEDED(newData) && newData.Code() != NS_ERROR_NOT_AVAILABLE) {
+ RejectAppend(newData, __func__);
+ return;
+ }
+ mProcessedInput += mInputBuffer->Length();
+
+ // 5. If the append state equals PARSING_INIT_SEGMENT, then run the
+ // following steps:
+ if (mSourceBufferAttributes->GetAppendState() == AppendState::PARSING_INIT_SEGMENT) {
+ if (mParser->InitSegmentRange().IsEmpty()) {
+ mInputBuffer = nullptr;
+ NeedMoreData();
+ return;
+ }
+ InitializationSegmentReceived();
+ return;
+ }
+ if (mSourceBufferAttributes->GetAppendState() == AppendState::PARSING_MEDIA_SEGMENT) {
+ // 1. If the first initialization segment received flag is false, then run the append error algorithm with the decode error parameter set to true and abort this algorithm.
+ if (!mFirstInitializationSegmentReceived) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ // We can't feed some demuxers (WebMDemuxer) with data that do not have
+ // monotonizally increasing timestamps. So we check if we have a
+ // discontinuity from the previous segment parsed.
+ // If so, recreate a new demuxer to ensure that the demuxer is only fed
+ // monotonically increasing data.
+ if (mNewMediaSegmentStarted) {
+ if (NS_SUCCEEDED(newData) && mLastParsedEndTime.isSome() &&
+ start < mLastParsedEndTime.ref().ToMicroseconds()) {
+ MSE_DEBUG("Re-creating demuxer");
+ ResetDemuxingState();
+ return;
+ }
+ if (NS_SUCCEEDED(newData) || !mParser->MediaSegmentRange().IsEmpty()) {
+ if (mPendingInputBuffer) {
+ // We now have a complete media segment header. We can resume parsing
+ // the data.
+ AppendDataToCurrentInputBuffer(mPendingInputBuffer);
+ mPendingInputBuffer = nullptr;
+ }
+ mNewMediaSegmentStarted = false;
+ } else {
+ // We don't have any data to demux yet, stash aside the data.
+ // This also handles the case:
+ // 2. If the input buffer does not contain a complete media segment header yet, then jump to the need more data step below.
+ if (!mPendingInputBuffer) {
+ mPendingInputBuffer = mInputBuffer;
+ } else {
+ mPendingInputBuffer->AppendElements(*mInputBuffer);
+ }
+ mInputBuffer = nullptr;
+ NeedMoreData();
+ return;
+ }
+ }
+
+ // 3. If the input buffer contains one or more complete coded frames, then run the coded frame processing algorithm.
+ RefPtr<TrackBuffersManager> self = this;
+ mProcessingRequest.Begin(CodedFrameProcessing()
+ ->Then(GetTaskQueue(), __func__,
+ [self] (bool aNeedMoreData) {
+ self->mProcessingRequest.Complete();
+ if (aNeedMoreData) {
+ self->NeedMoreData();
+ } else {
+ self->ScheduleSegmentParserLoop();
+ }
+ },
+ [self] (const MediaResult& aRejectValue) {
+ self->mProcessingRequest.Complete();
+ self->RejectAppend(aRejectValue, __func__);
+ }));
+ return;
+ }
+ }
+}
+
+void
+TrackBuffersManager::NeedMoreData()
+{
+ MSE_DEBUG("");
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentTask && mCurrentTask->GetType() == SourceBufferTask::Type::AppendBuffer);
+ MOZ_DIAGNOSTIC_ASSERT(mSourceBufferAttributes);
+
+ mCurrentTask->As<AppendBufferTask>()->mPromise.Resolve(
+ SourceBufferTask::AppendBufferResult(mActiveTrack,
+ *mSourceBufferAttributes),
+ __func__);
+ mSourceBufferAttributes = nullptr;
+ mCurrentTask = nullptr;
+ ProcessTasks();
+}
+
+void
+TrackBuffersManager::RejectAppend(const MediaResult& aRejectValue, const char* aName)
+{
+ MSE_DEBUG("rv=%u", aRejectValue.Code());
+ MOZ_DIAGNOSTIC_ASSERT(mCurrentTask && mCurrentTask->GetType() == SourceBufferTask::Type::AppendBuffer);
+
+ mCurrentTask->As<AppendBufferTask>()->mPromise.Reject(aRejectValue, __func__);
+ mSourceBufferAttributes = nullptr;
+ mCurrentTask = nullptr;
+ ProcessTasks();
+}
+
+void
+TrackBuffersManager::ScheduleSegmentParserLoop()
+{
+ GetTaskQueue()->Dispatch(NewRunnableMethod(this, &TrackBuffersManager::SegmentParserLoop));
+}
+
+void
+TrackBuffersManager::ShutdownDemuxers()
+{
+ if (mVideoTracks.mDemuxer) {
+ mVideoTracks.mDemuxer->BreakCycles();
+ mVideoTracks.mDemuxer = nullptr;
+ }
+ if (mAudioTracks.mDemuxer) {
+ mAudioTracks.mDemuxer->BreakCycles();
+ mAudioTracks.mDemuxer = nullptr;
+ }
+ // We shouldn't change mInputDemuxer while a demuxer init/reset request is
+ // being processed. See bug 1239983.
+ MOZ_DIAGNOSTIC_ASSERT(!mDemuxerInitRequest.Exists());
+ mInputDemuxer = nullptr;
+ mLastParsedEndTime.reset();
+}
+
+void
+TrackBuffersManager::CreateDemuxerforMIMEType()
+{
+ ShutdownDemuxers();
+
+ if (mType.LowerCaseEqualsLiteral("video/webm") ||
+ mType.LowerCaseEqualsLiteral("audio/webm")) {
+ mInputDemuxer = new WebMDemuxer(mCurrentInputBuffer, true /* IsMediaSource*/ );
+ return;
+ }
+
+#ifdef MOZ_FMP4
+ if (mType.LowerCaseEqualsLiteral("video/mp4") ||
+ mType.LowerCaseEqualsLiteral("audio/mp4")) {
+ mInputDemuxer = new MP4Demuxer(mCurrentInputBuffer);
+ return;
+ }
+#endif
+ NS_WARNING("Not supported (yet)");
+ return;
+}
+
+// We reset the demuxer by creating a new one and initializing it.
+void
+TrackBuffersManager::ResetDemuxingState()
+{
+ MOZ_ASSERT(mParser && mParser->HasInitData());
+ RecreateParser(true);
+ mCurrentInputBuffer = new SourceBufferResource(mType);
+ // The demuxer isn't initialized yet ; we don't want to notify it
+ // that data has been appended yet ; so we simply append the init segment
+ // to the resource.
+ mCurrentInputBuffer->AppendData(mParser->InitData());
+ CreateDemuxerforMIMEType();
+ if (!mInputDemuxer) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+ mDemuxerInitRequest.Begin(mInputDemuxer->Init()
+ ->Then(GetTaskQueue(), __func__,
+ this,
+ &TrackBuffersManager::OnDemuxerResetDone,
+ &TrackBuffersManager::OnDemuxerInitFailed));
+}
+
+void
+TrackBuffersManager::OnDemuxerResetDone(nsresult)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ mDemuxerInitRequest.Complete();
+ // mInputDemuxer shouldn't have been destroyed while a demuxer init/reset
+ // request was being processed. See bug 1239983.
+ MOZ_DIAGNOSTIC_ASSERT(mInputDemuxer);
+
+ // Recreate track demuxers.
+ uint32_t numVideos = mInputDemuxer->GetNumberTracks(TrackInfo::kVideoTrack);
+ if (numVideos) {
+ // We currently only handle the first video track.
+ mVideoTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ MOZ_ASSERT(mVideoTracks.mDemuxer);
+ }
+
+ uint32_t numAudios = mInputDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
+ if (numAudios) {
+ // We currently only handle the first audio track.
+ mAudioTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ MOZ_ASSERT(mAudioTracks.mDemuxer);
+ }
+
+ if (mPendingInputBuffer) {
+ // We had a partial media segment header stashed aside.
+ // Reparse its content so we can continue parsing the current input buffer.
+ int64_t start, end;
+ mParser->ParseStartAndEndTimestamps(mPendingInputBuffer, start, end);
+ mProcessedInput += mPendingInputBuffer->Length();
+ }
+
+ SegmentParserLoop();
+}
+
+void
+TrackBuffersManager::AppendDataToCurrentInputBuffer(MediaByteBuffer* aData)
+{
+ MOZ_ASSERT(mCurrentInputBuffer);
+ mCurrentInputBuffer->AppendData(aData);
+ mInputDemuxer->NotifyDataArrived();
+}
+
+void
+TrackBuffersManager::InitializationSegmentReceived()
+{
+ MOZ_ASSERT(mParser->HasCompleteInitData());
+
+ int64_t endInit = mParser->InitSegmentRange().mEnd;
+ if (mInputBuffer->Length() > mProcessedInput ||
+ int64_t(mProcessedInput - mInputBuffer->Length()) > endInit) {
+ // Something is not quite right with the data appended. Refuse it.
+ RejectAppend(MediaResult(NS_ERROR_FAILURE,
+ "Invalid state following initialization segment"),
+ __func__);
+ return;
+ }
+
+ mCurrentInputBuffer = new SourceBufferResource(mType);
+ // The demuxer isn't initialized yet ; we don't want to notify it
+ // that data has been appended yet ; so we simply append the init segment
+ // to the resource.
+ mCurrentInputBuffer->AppendData(mParser->InitData());
+ uint32_t length = endInit - (mProcessedInput - mInputBuffer->Length());
+ if (mInputBuffer->Length() == length) {
+ mInputBuffer = nullptr;
+ } else {
+ MOZ_RELEASE_ASSERT(length <= mInputBuffer->Length());
+ mInputBuffer->RemoveElementsAt(0, length);
+ }
+ CreateDemuxerforMIMEType();
+ if (!mInputDemuxer) {
+ NS_WARNING("TODO type not supported");
+ RejectAppend(NS_ERROR_DOM_NOT_SUPPORTED_ERR, __func__);
+ return;
+ }
+ mDemuxerInitRequest.Begin(mInputDemuxer->Init()
+ ->Then(GetTaskQueue(), __func__,
+ this,
+ &TrackBuffersManager::OnDemuxerInitDone,
+ &TrackBuffersManager::OnDemuxerInitFailed));
+}
+
+void
+TrackBuffersManager::OnDemuxerInitDone(nsresult)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_DIAGNOSTIC_ASSERT(mInputDemuxer, "mInputDemuxer has been destroyed");
+
+ mDemuxerInitRequest.Complete();
+
+ MediaInfo info;
+
+ uint32_t numVideos = mInputDemuxer->GetNumberTracks(TrackInfo::kVideoTrack);
+ if (numVideos) {
+ // We currently only handle the first video track.
+ mVideoTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kVideoTrack, 0);
+ MOZ_ASSERT(mVideoTracks.mDemuxer);
+ info.mVideo = *mVideoTracks.mDemuxer->GetInfo()->GetAsVideoInfo();
+ info.mVideo.mTrackId = 2;
+ }
+
+ uint32_t numAudios = mInputDemuxer->GetNumberTracks(TrackInfo::kAudioTrack);
+ if (numAudios) {
+ // We currently only handle the first audio track.
+ mAudioTracks.mDemuxer =
+ mInputDemuxer->GetTrackDemuxer(TrackInfo::kAudioTrack, 0);
+ MOZ_ASSERT(mAudioTracks.mDemuxer);
+ info.mAudio = *mAudioTracks.mDemuxer->GetInfo()->GetAsAudioInfo();
+ info.mAudio.mTrackId = 1;
+ }
+
+ int64_t videoDuration = numVideos ? info.mVideo.mDuration : 0;
+ int64_t audioDuration = numAudios ? info.mAudio.mDuration : 0;
+
+ int64_t duration = std::max(videoDuration, audioDuration);
+ // 1. Update the duration attribute if it currently equals NaN.
+ // Those steps are performed by the MediaSourceDecoder::SetInitialDuration
+ AbstractThread::MainThread()->Dispatch(NewRunnableMethod<int64_t>
+ (mParentDecoder,
+ &MediaSourceDecoder::SetInitialDuration,
+ duration ? duration : -1));
+
+ // 2. If the initialization segment has no audio, video, or text tracks, then
+ // run the append error algorithm with the decode error parameter set to true
+ // and abort these steps.
+ if (!numVideos && !numAudios) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ // 3. If the first initialization segment received flag is true, then run the following steps:
+ if (mFirstInitializationSegmentReceived) {
+ if (numVideos != mVideoTracks.mNumTracks ||
+ numAudios != mAudioTracks.mNumTracks ||
+ (numVideos && info.mVideo.mMimeType != mVideoTracks.mInfo->mMimeType) ||
+ (numAudios && info.mAudio.mMimeType != mAudioTracks.mInfo->mMimeType)) {
+ RejectAppend(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+ // 1. If more than one track for a single type are present (ie 2 audio tracks),
+ // then the Track IDs match the ones in the first initialization segment.
+ // TODO
+ // 2. Add the appropriate track descriptions from this initialization
+ // segment to each of the track buffers.
+ // TODO
+ // 3. Set the need random access point flag on all track buffers to true.
+ mVideoTracks.mNeedRandomAccessPoint = true;
+ mAudioTracks.mNeedRandomAccessPoint = true;
+ }
+
+ // 4. Let active track flag equal false.
+ bool activeTrack = false;
+
+ // Increase our stream id.
+ uint32_t streamID = sStreamSourceID++;
+
+ // 5. If the first initialization segment received flag is false, then run the following steps:
+ if (!mFirstInitializationSegmentReceived) {
+ mAudioTracks.mNumTracks = numAudios;
+ // TODO:
+ // 1. If the initialization segment contains tracks with codecs the user agent
+ // does not support, then run the append error algorithm with the decode
+ // error parameter set to true and abort these steps.
+
+ // 2. For each audio track in the initialization segment, run following steps:
+ // for (uint32_t i = 0; i < numAudios; i++) {
+ if (numAudios) {
+ // 1. Let audio byte stream track ID be the Track ID for the current track being processed.
+ // 2. Let audio language be a BCP 47 language tag for the language specified in the initialization segment for this track or an empty string if no language info is present.
+ // 3. If audio language equals an empty string or the 'und' BCP 47 value, then run the default track language algorithm with byteStreamTrackID set to audio byte stream track ID and type set to "audio" and assign the value returned by the algorithm to audio language.
+ // 4. Let audio label be a label specified in the initialization segment for this track or an empty string if no label info is present.
+ // 5. If audio label equals an empty string, then run the default track label algorithm with byteStreamTrackID set to audio byte stream track ID and type set to "audio" and assign the value returned by the algorithm to audio label.
+ // 6. Let audio kinds be an array of kind strings specified in the initialization segment for this track or an empty array if no kind information is provided.
+ // 7. If audio kinds equals an empty array, then run the default track kinds algorithm with byteStreamTrackID set to audio byte stream track ID and type set to "audio" and assign the value returned by the algorithm to audio kinds.
+ // 8. For each value in audio kinds, run the following steps:
+ // 1. Let current audio kind equal the value from audio kinds for this iteration of the loop.
+ // 2. Let new audio track be a new AudioTrack object.
+ // 3. Generate a unique ID and assign it to the id property on new audio track.
+ // 4. Assign audio language to the language property on new audio track.
+ // 5. Assign audio label to the label property on new audio track.
+ // 6. Assign current audio kind to the kind property on new audio track.
+ // 7. If audioTracks.length equals 0, then run the following steps:
+ // 1. Set the enabled property on new audio track to true.
+ // 2. Set active track flag to true.
+ activeTrack = true;
+ // 8. Add new audio track to the audioTracks attribute on this SourceBuffer object.
+ // 9. Queue a task to fire a trusted event named addtrack, that does not bubble and is not cancelable, and that uses the TrackEvent interface, at the AudioTrackList object referenced by the audioTracks attribute on this SourceBuffer object.
+ // 10. Add new audio track to the audioTracks attribute on the HTMLMediaElement.
+ // 11. Queue a task to fire a trusted event named addtrack, that does not bubble and is not cancelable, and that uses the TrackEvent interface, at the AudioTrackList object referenced by the audioTracks attribute on the HTMLMediaElement.
+ mAudioTracks.mBuffers.AppendElement(TrackBuffer());
+ // 10. Add the track description for this track to the track buffer.
+ mAudioTracks.mInfo = new SharedTrackInfo(info.mAudio, streamID);
+ mAudioTracks.mLastInfo = mAudioTracks.mInfo;
+ }
+
+ mVideoTracks.mNumTracks = numVideos;
+ // 3. For each video track in the initialization segment, run following steps:
+ // for (uint32_t i = 0; i < numVideos; i++) {
+ if (numVideos) {
+ // 1. Let video byte stream track ID be the Track ID for the current track being processed.
+ // 2. Let video language be a BCP 47 language tag for the language specified in the initialization segment for this track or an empty string if no language info is present.
+ // 3. If video language equals an empty string or the 'und' BCP 47 value, then run the default track language algorithm with byteStreamTrackID set to video byte stream track ID and type set to "video" and assign the value returned by the algorithm to video language.
+ // 4. Let video label be a label specified in the initialization segment for this track or an empty string if no label info is present.
+ // 5. If video label equals an empty string, then run the default track label algorithm with byteStreamTrackID set to video byte stream track ID and type set to "video" and assign the value returned by the algorithm to video label.
+ // 6. Let video kinds be an array of kind strings specified in the initialization segment for this track or an empty array if no kind information is provided.
+ // 7. If video kinds equals an empty array, then run the default track kinds algorithm with byteStreamTrackID set to video byte stream track ID and type set to "video" and assign the value returned by the algorithm to video kinds.
+ // 8. For each value in video kinds, run the following steps:
+ // 1. Let current video kind equal the value from video kinds for this iteration of the loop.
+ // 2. Let new video track be a new VideoTrack object.
+ // 3. Generate a unique ID and assign it to the id property on new video track.
+ // 4. Assign video language to the language property on new video track.
+ // 5. Assign video label to the label property on new video track.
+ // 6. Assign current video kind to the kind property on new video track.
+ // 7. If videoTracks.length equals 0, then run the following steps:
+ // 1. Set the selected property on new video track to true.
+ // 2. Set active track flag to true.
+ activeTrack = true;
+ // 8. Add new video track to the videoTracks attribute on this SourceBuffer object.
+ // 9. Queue a task to fire a trusted event named addtrack, that does not bubble and is not cancelable, and that uses the TrackEvent interface, at the VideoTrackList object referenced by the videoTracks attribute on this SourceBuffer object.
+ // 10. Add new video track to the videoTracks attribute on the HTMLMediaElement.
+ // 11. Queue a task to fire a trusted event named addtrack, that does not bubble and is not cancelable, and that uses the TrackEvent interface, at the VideoTrackList object referenced by the videoTracks attribute on the HTMLMediaElement.
+ mVideoTracks.mBuffers.AppendElement(TrackBuffer());
+ // 10. Add the track description for this track to the track buffer.
+ mVideoTracks.mInfo = new SharedTrackInfo(info.mVideo, streamID);
+ mVideoTracks.mLastInfo = mVideoTracks.mInfo;
+ }
+ // 4. For each text track in the initialization segment, run following steps:
+ // 5. If active track flag equals true, then run the following steps:
+ // This is handled by SourceBuffer once the promise is resolved.
+ if (activeTrack) {
+ mActiveTrack = true;
+ }
+
+ // 6. Set first initialization segment received flag to true.
+ mFirstInitializationSegmentReceived = true;
+ } else {
+ mAudioTracks.mLastInfo = new SharedTrackInfo(info.mAudio, streamID);
+ mVideoTracks.mLastInfo = new SharedTrackInfo(info.mVideo, streamID);
+ }
+
+ UniquePtr<EncryptionInfo> crypto = mInputDemuxer->GetCrypto();
+ if (crypto && crypto->IsEncrypted()) {
+ // Try and dispatch 'encrypted'. Won't go if ready state still HAVE_NOTHING.
+ for (uint32_t i = 0; i < crypto->mInitDatas.Length(); i++) {
+ NS_DispatchToMainThread(
+ new DispatchKeyNeededEvent(mParentDecoder, crypto->mInitDatas[i].mInitData,
+ crypto->mInitDatas[i].mType));
+ }
+ info.mCrypto = *crypto;
+ // We clear our crypto init data array, so the MediaFormatReader will
+ // not emit an encrypted event for the same init data again.
+ info.mCrypto.mInitDatas.Clear();
+ }
+
+ {
+ MonitorAutoLock mon(mMonitor);
+ mInfo = info;
+ }
+
+ // We now have a valid init data ; we can store it for later use.
+ mInitData = mParser->InitData();
+
+ // 3. Remove the initialization segment bytes from the beginning of the input buffer.
+ // This step has already been done in InitializationSegmentReceived when we
+ // transferred the content into mCurrentInputBuffer.
+ mCurrentInputBuffer->EvictAll();
+ mInputDemuxer->NotifyDataRemoved();
+ RecreateParser(true);
+
+ // 4. Set append state to WAITING_FOR_SEGMENT.
+ SetAppendState(AppendState::WAITING_FOR_SEGMENT);
+ // 5. Jump to the loop top step above.
+ ScheduleSegmentParserLoop();
+}
+
+void
+TrackBuffersManager::OnDemuxerInitFailed(const MediaResult& aError)
+{
+ MOZ_ASSERT(aError != NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA);
+ mDemuxerInitRequest.Complete();
+
+ RejectAppend(aError, __func__);
+}
+
+RefPtr<TrackBuffersManager::CodedFrameProcessingPromise>
+TrackBuffersManager::CodedFrameProcessing()
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MOZ_ASSERT(mProcessingPromise.IsEmpty());
+
+ MediaByteRange mediaRange = mParser->MediaSegmentRange();
+ if (mediaRange.IsEmpty()) {
+ AppendDataToCurrentInputBuffer(mInputBuffer);
+ mInputBuffer = nullptr;
+ } else {
+ MOZ_ASSERT(mProcessedInput >= mInputBuffer->Length());
+ if (int64_t(mProcessedInput - mInputBuffer->Length()) > mediaRange.mEnd) {
+ // Something is not quite right with the data appended. Refuse it.
+ // This would typically happen if the previous media segment was partial
+ // yet a new complete media segment was added.
+ return CodedFrameProcessingPromise::CreateAndReject(NS_ERROR_FAILURE, __func__);
+ }
+ // The mediaRange is offset by the init segment position previously added.
+ uint32_t length =
+ mediaRange.mEnd - (mProcessedInput - mInputBuffer->Length());
+ if (!length) {
+ // We've completed our earlier media segment and no new data is to be
+ // processed. This happens with some containers that can't detect that a
+ // media segment is ending until a new one starts.
+ RefPtr<CodedFrameProcessingPromise> p = mProcessingPromise.Ensure(__func__);
+ CompleteCodedFrameProcessing();
+ return p;
+ }
+ RefPtr<MediaByteBuffer> segment = new MediaByteBuffer;
+ if (!segment->AppendElements(mInputBuffer->Elements(), length, fallible)) {
+ return CodedFrameProcessingPromise::CreateAndReject(NS_ERROR_OUT_OF_MEMORY, __func__);
+ }
+ AppendDataToCurrentInputBuffer(segment);
+ mInputBuffer->RemoveElementsAt(0, length);
+ }
+
+ RefPtr<CodedFrameProcessingPromise> p = mProcessingPromise.Ensure(__func__);
+
+ DoDemuxVideo();
+
+ return p;
+}
+
+void
+TrackBuffersManager::OnDemuxFailed(TrackType aTrack,
+ const MediaResult& aError)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MSE_DEBUG("Failed to demux %s, failure:%u",
+ aTrack == TrackType::kVideoTrack ? "video" : "audio", aError.Code());
+ switch (aError.Code()) {
+ case NS_ERROR_DOM_MEDIA_END_OF_STREAM:
+ case NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA:
+ if (aTrack == TrackType::kVideoTrack) {
+ DoDemuxAudio();
+ } else {
+ CompleteCodedFrameProcessing();
+ }
+ break;
+ default:
+ RejectProcessing(aError, __func__);
+ break;
+ }
+}
+
+void
+TrackBuffersManager::DoDemuxVideo()
+{
+ MOZ_ASSERT(OnTaskQueue());
+ if (!HasVideo()) {
+ DoDemuxAudio();
+ return;
+ }
+ mVideoTracks.mDemuxRequest.Begin(mVideoTracks.mDemuxer->GetSamples(-1)
+ ->Then(GetTaskQueue(), __func__, this,
+ &TrackBuffersManager::OnVideoDemuxCompleted,
+ &TrackBuffersManager::OnVideoDemuxFailed));
+}
+
+void
+TrackBuffersManager::OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MSE_DEBUG("%d video samples demuxed", aSamples->mSamples.Length());
+ mVideoTracks.mDemuxRequest.Complete();
+ mVideoTracks.mQueuedSamples.AppendElements(aSamples->mSamples);
+ DoDemuxAudio();
+}
+
+void
+TrackBuffersManager::DoDemuxAudio()
+{
+ MOZ_ASSERT(OnTaskQueue());
+ if (!HasAudio()) {
+ CompleteCodedFrameProcessing();
+ return;
+ }
+ mAudioTracks.mDemuxRequest.Begin(mAudioTracks.mDemuxer->GetSamples(-1)
+ ->Then(GetTaskQueue(), __func__, this,
+ &TrackBuffersManager::OnAudioDemuxCompleted,
+ &TrackBuffersManager::OnAudioDemuxFailed));
+}
+
+void
+TrackBuffersManager::OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ MSE_DEBUG("%d audio samples demuxed", aSamples->mSamples.Length());
+ mAudioTracks.mDemuxRequest.Complete();
+ mAudioTracks.mQueuedSamples.AppendElements(aSamples->mSamples);
+ CompleteCodedFrameProcessing();
+}
+
+void
+TrackBuffersManager::CompleteCodedFrameProcessing()
+{
+ MOZ_ASSERT(OnTaskQueue());
+
+ // 1. For each coded frame in the media segment run the following steps:
+ // Coded Frame Processing steps 1.1 to 1.21.
+
+ if (mSourceBufferAttributes->GetAppendMode() == SourceBufferAppendMode::Sequence &&
+ mVideoTracks.mQueuedSamples.Length() && mAudioTracks.mQueuedSamples.Length()) {
+ // When we are in sequence mode, the order in which we process the frames is
+ // important as it determines the future value of timestampOffset.
+ // So we process the earliest sample first. See bug 1293576.
+ TimeInterval videoInterval =
+ PresentationInterval(mVideoTracks.mQueuedSamples);
+ TimeInterval audioInterval =
+ PresentationInterval(mAudioTracks.mQueuedSamples);
+ if (audioInterval.mStart < videoInterval.mStart) {
+ ProcessFrames(mAudioTracks.mQueuedSamples, mAudioTracks);
+ ProcessFrames(mVideoTracks.mQueuedSamples, mVideoTracks);
+ } else {
+ ProcessFrames(mVideoTracks.mQueuedSamples, mVideoTracks);
+ ProcessFrames(mAudioTracks.mQueuedSamples, mAudioTracks);
+ }
+ } else {
+ ProcessFrames(mVideoTracks.mQueuedSamples, mVideoTracks);
+ ProcessFrames(mAudioTracks.mQueuedSamples, mAudioTracks);
+ }
+
+#if defined(DEBUG)
+ if (HasVideo()) {
+ const auto& track = mVideoTracks.GetTrackBuffer();
+ MOZ_ASSERT(track.IsEmpty() || track[0]->mKeyframe);
+ for (uint32_t i = 1; i < track.Length(); i++) {
+ MOZ_ASSERT((track[i-1]->mTrackInfo->GetID() == track[i]->mTrackInfo->GetID() && track[i-1]->mTimecode <= track[i]->mTimecode) ||
+ track[i]->mKeyframe);
+ }
+ }
+ if (HasAudio()) {
+ const auto& track = mAudioTracks.GetTrackBuffer();
+ MOZ_ASSERT(track.IsEmpty() || track[0]->mKeyframe);
+ for (uint32_t i = 1; i < track.Length(); i++) {
+ MOZ_ASSERT((track[i-1]->mTrackInfo->GetID() == track[i]->mTrackInfo->GetID() && track[i-1]->mTimecode <= track[i]->mTimecode) ||
+ track[i]->mKeyframe);
+ }
+ }
+#endif
+
+ mVideoTracks.mQueuedSamples.Clear();
+ mAudioTracks.mQueuedSamples.Clear();
+
+ UpdateBufferedRanges();
+
+ // Update our reported total size.
+ mSizeSourceBuffer = mVideoTracks.mSizeBuffer + mAudioTracks.mSizeBuffer;
+
+ // Return to step 6.4 of Segment Parser Loop algorithm
+ // 4. If this SourceBuffer is full and cannot accept more media data, then set the buffer full flag to true.
+ if (mSizeSourceBuffer >= EvictionThreshold()) {
+ mBufferFull = true;
+ }
+
+ // 5. If the input buffer does not contain a complete media segment, then jump to the need more data step below.
+ if (mParser->MediaSegmentRange().IsEmpty()) {
+ ResolveProcessing(true, __func__);
+ return;
+ }
+
+ mLastParsedEndTime = Some(std::max(mAudioTracks.mLastParsedEndTime,
+ mVideoTracks.mLastParsedEndTime));
+
+ // 6. Remove the media segment bytes from the beginning of the input buffer.
+ // Clear our demuxer from any already processed data.
+ int64_t safeToEvict = std::min(
+ HasVideo()
+ ? mVideoTracks.mDemuxer->GetEvictionOffset(mVideoTracks.mLastParsedEndTime)
+ : INT64_MAX,
+ HasAudio()
+ ? mAudioTracks.mDemuxer->GetEvictionOffset(mAudioTracks.mLastParsedEndTime)
+ : INT64_MAX);
+ ErrorResult rv;
+ mCurrentInputBuffer->EvictBefore(safeToEvict, rv);
+ if (rv.Failed()) {
+ rv.SuppressException();
+ RejectProcessing(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return;
+ }
+
+ mInputDemuxer->NotifyDataRemoved();
+ RecreateParser(true);
+
+ // 7. Set append state to WAITING_FOR_SEGMENT.
+ SetAppendState(AppendState::WAITING_FOR_SEGMENT);
+
+ // 8. Jump to the loop top step above.
+ ResolveProcessing(false, __func__);
+}
+
+void
+TrackBuffersManager::RejectProcessing(const MediaResult& aRejectValue, const char* aName)
+{
+ mProcessingPromise.RejectIfExists(aRejectValue, __func__);
+}
+
+void
+TrackBuffersManager::ResolveProcessing(bool aResolveValue, const char* aName)
+{
+ mProcessingPromise.ResolveIfExists(aResolveValue, __func__);
+}
+
+void
+TrackBuffersManager::CheckSequenceDiscontinuity(const TimeUnit& aPresentationTime)
+{
+ if (mSourceBufferAttributes->GetAppendMode() == SourceBufferAppendMode::Sequence &&
+ mSourceBufferAttributes->HaveGroupStartTimestamp()) {
+ mSourceBufferAttributes->SetTimestampOffset(
+ mSourceBufferAttributes->GetGroupStartTimestamp() - aPresentationTime);
+ mSourceBufferAttributes->SetGroupEndTimestamp(
+ mSourceBufferAttributes->GetGroupStartTimestamp());
+ mVideoTracks.mNeedRandomAccessPoint = true;
+ mAudioTracks.mNeedRandomAccessPoint = true;
+ mSourceBufferAttributes->ResetGroupStartTimestamp();
+ }
+}
+
+TimeInterval
+TrackBuffersManager::PresentationInterval(const TrackBuffer& aSamples) const
+{
+ TimeInterval presentationInterval =
+ TimeInterval(TimeUnit::FromMicroseconds(aSamples[0]->mTime),
+ TimeUnit::FromMicroseconds(aSamples[0]->GetEndTime()));
+
+ for (uint32_t i = 1; i < aSamples.Length(); i++) {
+ auto& sample = aSamples[i];
+ presentationInterval = presentationInterval.Span(
+ TimeInterval(TimeUnit::FromMicroseconds(sample->mTime),
+ TimeUnit::FromMicroseconds(sample->GetEndTime())));
+ }
+ return presentationInterval;
+}
+
+void
+TrackBuffersManager::ProcessFrames(TrackBuffer& aSamples, TrackData& aTrackData)
+{
+ if (!aSamples.Length()) {
+ return;
+ }
+
+ // 1. If generate timestamps flag equals true
+ // Let presentation timestamp equal 0.
+ // Otherwise
+ // Let presentation timestamp be a double precision floating point representation of the coded frame's presentation timestamp in seconds.
+ TimeUnit presentationTimestamp = mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeUnit() : TimeUnit::FromMicroseconds(aSamples[0]->mTime);
+
+ // 3. If mode equals "sequence" and group start timestamp is set, then run the following steps:
+ CheckSequenceDiscontinuity(presentationTimestamp);
+
+ // 5. Let track buffer equal the track buffer that the coded frame will be added to.
+ auto& trackBuffer = aTrackData;
+
+ // Some videos do not exactly start at 0, but instead a small negative value.
+ // To avoid evicting the starting frame of those videos, we allow a leeway
+ // of +- mLongestFrameDuration on the append window start.
+ // We only apply the leeway with the default append window start of 0
+ // otherwise do as per spec.
+ TimeInterval targetWindow = mAppendWindow.mStart != TimeUnit::FromSeconds(0)
+ ? mAppendWindow
+ : TimeInterval(mAppendWindow.mStart, mAppendWindow.mEnd,
+ trackBuffer.mLastFrameDuration.isSome()
+ ? trackBuffer.mLongestFrameDuration
+ : TimeUnit::FromMicroseconds(aSamples[0]->mDuration));
+
+ TimeIntervals samplesRange;
+ uint32_t sizeNewSamples = 0;
+ TrackBuffer samples; // array that will contain the frames to be added
+ // to our track buffer.
+
+ // We assume that no frames are contiguous within a media segment and as such
+ // don't need to check for discontinuity except for the first frame and should
+ // a frame be ignored due to the target window.
+ bool needDiscontinuityCheck = true;
+
+ // Highest presentation time seen in samples block.
+ TimeUnit highestSampleTime;
+
+ if (aSamples.Length()) {
+ aTrackData.mLastParsedEndTime = TimeUnit();
+ }
+
+ for (auto& sample : aSamples) {
+ SAMPLE_DEBUG("Processing %s frame(pts:%lld end:%lld, dts:%lld, duration:%lld, "
+ "kf:%d)",
+ aTrackData.mInfo->mMimeType.get(),
+ sample->mTime,
+ sample->GetEndTime(),
+ sample->mTimecode,
+ sample->mDuration,
+ sample->mKeyframe);
+
+ const TimeUnit sampleEndTime =
+ TimeUnit::FromMicroseconds(sample->GetEndTime());
+ if (sampleEndTime > aTrackData.mLastParsedEndTime) {
+ aTrackData.mLastParsedEndTime = sampleEndTime;
+ }
+
+ // We perform step 10 right away as we can't do anything should a keyframe
+ // be needed until we have one.
+
+ // 10. If the need random access point flag on track buffer equals true, then run the following steps:
+ if (trackBuffer.mNeedRandomAccessPoint) {
+ // 1. If the coded frame is not a random access point, then drop the coded frame and jump to the top of the loop to start processing the next coded frame.
+ if (!sample->mKeyframe) {
+ continue;
+ }
+ // 2. Set the need random access point flag on track buffer to false.
+ trackBuffer.mNeedRandomAccessPoint = false;
+ }
+
+ // We perform step 1,2 and 4 at once:
+ // 1. If generate timestamps flag equals true:
+ // Let presentation timestamp equal 0.
+ // Let decode timestamp equal 0.
+ // Otherwise:
+ // Let presentation timestamp be a double precision floating point representation of the coded frame's presentation timestamp in seconds.
+ // Let decode timestamp be a double precision floating point representation of the coded frame's decode timestamp in seconds.
+
+ // 2. Let frame duration be a double precision floating point representation of the coded frame's duration in seconds.
+ // Step 3 is performed earlier or when a discontinuity has been detected.
+ // 4. If timestampOffset is not 0, then run the following steps:
+
+ TimeUnit sampleTime = TimeUnit::FromMicroseconds(sample->mTime);
+ TimeUnit sampleTimecode = TimeUnit::FromMicroseconds(sample->mTimecode);
+ TimeUnit sampleDuration = TimeUnit::FromMicroseconds(sample->mDuration);
+ TimeUnit timestampOffset = mSourceBufferAttributes->GetTimestampOffset();
+
+ TimeInterval sampleInterval =
+ mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeInterval(timestampOffset, timestampOffset + sampleDuration)
+ : TimeInterval(timestampOffset + sampleTime,
+ timestampOffset + sampleTime + sampleDuration);
+ TimeUnit decodeTimestamp =
+ mSourceBufferAttributes->mGenerateTimestamps
+ ? timestampOffset
+ : timestampOffset + sampleTimecode;
+
+ // 6. If last decode timestamp for track buffer is set and decode timestamp is less than last decode timestamp:
+ // OR
+ // If last decode timestamp for track buffer is set and the difference between decode timestamp and last decode timestamp is greater than 2 times last frame duration:
+
+ if (needDiscontinuityCheck && trackBuffer.mLastDecodeTimestamp.isSome() &&
+ (decodeTimestamp < trackBuffer.mLastDecodeTimestamp.ref() ||
+ (decodeTimestamp - trackBuffer.mLastDecodeTimestamp.ref()
+ > 2 * trackBuffer.mLongestFrameDuration))) {
+ MSE_DEBUG("Discontinuity detected.");
+ SourceBufferAppendMode appendMode = mSourceBufferAttributes->GetAppendMode();
+
+ // 1a. If mode equals "segments":
+ if (appendMode == SourceBufferAppendMode::Segments) {
+ // Set group end timestamp to presentation timestamp.
+ mSourceBufferAttributes->SetGroupEndTimestamp(sampleInterval.mStart);
+ }
+ // 1b. If mode equals "sequence":
+ if (appendMode == SourceBufferAppendMode::Sequence) {
+ // Set group start timestamp equal to the group end timestamp.
+ mSourceBufferAttributes->SetGroupStartTimestamp(
+ mSourceBufferAttributes->GetGroupEndTimestamp());
+ }
+ for (auto& track : GetTracksList()) {
+ // 2. Unset the last decode timestamp on all track buffers.
+ // 3. Unset the last frame duration on all track buffers.
+ // 4. Unset the highest end timestamp on all track buffers.
+ // 5. Set the need random access point flag on all track buffers to true.
+ track->ResetAppendState();
+ }
+ // 6. Jump to the Loop Top step above to restart processing of the current coded frame.
+ // Rather that restarting the process for the frame, we run the first
+ // steps again instead.
+ // 3. If mode equals "sequence" and group start timestamp is set, then run the following steps:
+ TimeUnit presentationTimestamp = mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeUnit() : sampleTime;
+ CheckSequenceDiscontinuity(presentationTimestamp);
+
+ if (!sample->mKeyframe) {
+ continue;
+ }
+ if (appendMode == SourceBufferAppendMode::Sequence) {
+ // mSourceBufferAttributes->GetTimestampOffset() was modified during CheckSequenceDiscontinuity.
+ // We need to update our variables.
+ timestampOffset = mSourceBufferAttributes->GetTimestampOffset();
+ sampleInterval =
+ mSourceBufferAttributes->mGenerateTimestamps
+ ? TimeInterval(timestampOffset, timestampOffset + sampleDuration)
+ : TimeInterval(timestampOffset + sampleTime,
+ timestampOffset + sampleTime + sampleDuration);
+ decodeTimestamp =
+ mSourceBufferAttributes->mGenerateTimestamps
+ ? timestampOffset
+ : timestampOffset + sampleTimecode;
+ }
+ trackBuffer.mNeedRandomAccessPoint = false;
+ needDiscontinuityCheck = false;
+ }
+
+ // 7. Let frame end timestamp equal the sum of presentation timestamp and frame duration.
+ // This is sampleInterval.mEnd
+
+ // 8. If presentation timestamp is less than appendWindowStart, then set the need random access point flag to true, drop the coded frame, and jump to the top of the loop to start processing the next coded frame.
+ // 9. If frame end timestamp is greater than appendWindowEnd, then set the need random access point flag to true, drop the coded frame, and jump to the top of the loop to start processing the next coded frame.
+ if (!targetWindow.ContainsWithStrictEnd(sampleInterval)) {
+ if (samples.Length()) {
+ // We are creating a discontinuity in the samples.
+ // Insert the samples processed so far.
+ InsertFrames(samples, samplesRange, trackBuffer);
+ samples.Clear();
+ samplesRange = TimeIntervals();
+ trackBuffer.mSizeBuffer += sizeNewSamples;
+ sizeNewSamples = 0;
+ UpdateHighestTimestamp(trackBuffer, highestSampleTime);
+ }
+ trackBuffer.mNeedRandomAccessPoint = true;
+ needDiscontinuityCheck = true;
+ continue;
+ }
+
+ samplesRange += sampleInterval;
+ sizeNewSamples += sample->ComputedSizeOfIncludingThis();
+ sample->mTime = sampleInterval.mStart.ToMicroseconds();
+ sample->mTimecode = decodeTimestamp.ToMicroseconds();
+ sample->mTrackInfo = trackBuffer.mLastInfo;
+ samples.AppendElement(sample);
+
+ // Steps 11,12,13,14, 15 and 16 will be done in one block in InsertFrames.
+
+ trackBuffer.mLongestFrameDuration =
+ trackBuffer.mLastFrameDuration.isSome()
+ ? sample->mKeyframe
+ ? sampleDuration
+ : std::max(sampleDuration, trackBuffer.mLongestFrameDuration)
+ : sampleDuration;
+
+ // 17. Set last decode timestamp for track buffer to decode timestamp.
+ trackBuffer.mLastDecodeTimestamp = Some(decodeTimestamp);
+ // 18. Set last frame duration for track buffer to frame duration.
+ trackBuffer.mLastFrameDuration = Some(sampleDuration);
+
+ // 19. If highest end timestamp for track buffer is unset or frame end timestamp is greater than highest end timestamp, then set highest end timestamp for track buffer to frame end timestamp.
+ if (trackBuffer.mHighestEndTimestamp.isNothing() ||
+ sampleInterval.mEnd > trackBuffer.mHighestEndTimestamp.ref()) {
+ trackBuffer.mHighestEndTimestamp = Some(sampleInterval.mEnd);
+ }
+ if (sampleInterval.mStart > highestSampleTime) {
+ highestSampleTime = sampleInterval.mStart;
+ }
+ // 20. If frame end timestamp is greater than group end timestamp, then set group end timestamp equal to frame end timestamp.
+ if (sampleInterval.mEnd > mSourceBufferAttributes->GetGroupEndTimestamp()) {
+ mSourceBufferAttributes->SetGroupEndTimestamp(sampleInterval.mEnd);
+ }
+ // 21. If generate timestamps flag equals true, then set timestampOffset equal to frame end timestamp.
+ if (mSourceBufferAttributes->mGenerateTimestamps) {
+ mSourceBufferAttributes->SetTimestampOffset(sampleInterval.mEnd);
+ }
+ }
+
+ if (samples.Length()) {
+ InsertFrames(samples, samplesRange, trackBuffer);
+ trackBuffer.mSizeBuffer += sizeNewSamples;
+ UpdateHighestTimestamp(trackBuffer, highestSampleTime);
+ }
+}
+
+bool
+TrackBuffersManager::CheckNextInsertionIndex(TrackData& aTrackData,
+ const TimeUnit& aSampleTime)
+{
+ if (aTrackData.mNextInsertionIndex.isSome()) {
+ return true;
+ }
+
+ const TrackBuffer& data = aTrackData.GetTrackBuffer();
+
+ if (data.IsEmpty() || aSampleTime < aTrackData.mBufferedRanges.GetStart()) {
+ aTrackData.mNextInsertionIndex = Some(0u);
+ return true;
+ }
+
+ // Find which discontinuity we should insert the frame before.
+ TimeInterval target;
+ for (const auto& interval : aTrackData.mBufferedRanges) {
+ if (aSampleTime < interval.mStart) {
+ target = interval;
+ break;
+ }
+ }
+ if (target.IsEmpty()) {
+ // No target found, it will be added at the end of the track buffer.
+ aTrackData.mNextInsertionIndex = Some(uint32_t(data.Length()));
+ return true;
+ }
+ // We now need to find the first frame of the searched interval.
+ // We will insert our new frames right before.
+ for (uint32_t i = 0; i < data.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = data[i];
+ if (sample->mTime >= target.mStart.ToMicroseconds() ||
+ sample->GetEndTime() > target.mStart.ToMicroseconds()) {
+ aTrackData.mNextInsertionIndex = Some(i);
+ return true;
+ }
+ }
+ NS_ASSERTION(false, "Insertion Index Not Found");
+ return false;
+}
+
+void
+TrackBuffersManager::InsertFrames(TrackBuffer& aSamples,
+ const TimeIntervals& aIntervals,
+ TrackData& aTrackData)
+{
+ // 5. Let track buffer equal the track buffer that the coded frame will be added to.
+ auto& trackBuffer = aTrackData;
+
+ MSE_DEBUGV("Processing %d %s frames(start:%lld end:%lld)",
+ aSamples.Length(),
+ aTrackData.mInfo->mMimeType.get(),
+ aIntervals.GetStart().ToMicroseconds(),
+ aIntervals.GetEnd().ToMicroseconds());
+
+ // TODO: Handle splicing of audio (and text) frames.
+ // 11. Let spliced audio frame be an unset variable for holding audio splice information
+ // 12. Let spliced timed text frame be an unset variable for holding timed text splice information
+
+ // 13. If last decode timestamp for track buffer is unset and presentation timestamp falls within the presentation interval of a coded frame in track buffer,then run the following steps:
+ // For now we only handle replacing existing frames with the new ones. So we
+ // skip this step.
+
+ // 14. Remove existing coded frames in track buffer:
+ // a) If highest end timestamp for track buffer is not set:
+ // Remove all coded frames from track buffer that have a presentation timestamp greater than or equal to presentation timestamp and less than frame end timestamp.
+ // b) If highest end timestamp for track buffer is set and less than or equal to presentation timestamp:
+ // Remove all coded frames from track buffer that have a presentation timestamp greater than or equal to highest end timestamp and less than frame end timestamp
+
+ // There is an ambiguity on how to remove frames, which was lodged with:
+ // https://www.w3.org/Bugs/Public/show_bug.cgi?id=28710, implementing as per
+ // bug description.
+
+ // 15. Remove decoding dependencies of the coded frames removed in the previous step:
+ // Remove all coded frames between the coded frames removed in the previous step and the next random access point after those removed frames.
+
+ TimeIntervals intersection = trackBuffer.mBufferedRanges;
+ intersection.Intersection(aIntervals);
+
+ if (intersection.Length()) {
+ if (aSamples[0]->mKeyframe &&
+ (mType.LowerCaseEqualsLiteral("video/webm") ||
+ mType.LowerCaseEqualsLiteral("audio/webm"))) {
+ // We are starting a new GOP, we do not have to worry about breaking an
+ // existing current coded frame group. Reset the next insertion index
+ // so the search for when to start our frames removal can be exhaustive.
+ // This is a workaround for bug 1276184 and only until either bug 1277733
+ // or bug 1209386 is fixed.
+ // With the webm container, we can't always properly determine the
+ // duration of the last frame, which may cause the last frame of a cluster
+ // to overlap the following frame.
+ trackBuffer.mNextInsertionIndex.reset();
+ }
+ uint32_t index =
+ RemoveFrames(aIntervals, trackBuffer, trackBuffer.mNextInsertionIndex.refOr(0));
+ if (index) {
+ trackBuffer.mNextInsertionIndex = Some(index);
+ }
+ }
+
+ // 16. Add the coded frame with the presentation timestamp, decode timestamp, and frame duration to the track buffer.
+ if (!CheckNextInsertionIndex(aTrackData,
+ TimeUnit::FromMicroseconds(aSamples[0]->mTime))) {
+ RejectProcessing(NS_ERROR_FAILURE, __func__);
+ return;
+ }
+
+ // Adjust our demuxing index if necessary.
+ if (trackBuffer.mNextGetSampleIndex.isSome()) {
+ if (trackBuffer.mNextInsertionIndex.ref() == trackBuffer.mNextGetSampleIndex.ref() &&
+ aIntervals.GetEnd() >= trackBuffer.mNextSampleTime) {
+ MSE_DEBUG("Next sample to be played got overwritten");
+ trackBuffer.mNextGetSampleIndex.reset();
+ ResetEvictionIndex(trackBuffer);
+ } else if (trackBuffer.mNextInsertionIndex.ref() <= trackBuffer.mNextGetSampleIndex.ref()) {
+ trackBuffer.mNextGetSampleIndex.ref() += aSamples.Length();
+ // We could adjust the eviction index so that the new data gets added to
+ // the evictable amount (as it is prior currentTime). However, considering
+ // new data is being added prior the current playback, it's likely that
+ // this data will be played next, and as such we probably don't want to
+ // have it evicted too early. So instead reset the eviction index instead.
+ ResetEvictionIndex(trackBuffer);
+ }
+ }
+
+ TrackBuffer& data = trackBuffer.GetTrackBuffer();
+ data.InsertElementsAt(trackBuffer.mNextInsertionIndex.ref(), aSamples);
+ trackBuffer.mNextInsertionIndex.ref() += aSamples.Length();
+
+ // Update our buffered range with new sample interval.
+ trackBuffer.mBufferedRanges += aIntervals;
+ // We allow a fuzz factor in our interval of half a frame length,
+ // as fuzz is +/- value, giving an effective leeway of a full frame
+ // length.
+ if (aIntervals.Length()) {
+ TimeIntervals range(aIntervals);
+ range.SetFuzz(trackBuffer.mLongestFrameDuration / 2);
+ trackBuffer.mSanitizedBufferedRanges += range;
+ }
+}
+
+void
+TrackBuffersManager::UpdateHighestTimestamp(TrackData& aTrackData,
+ const media::TimeUnit& aHighestTime)
+{
+ if (aHighestTime > aTrackData.mHighestStartTimestamp) {
+ MonitorAutoLock mon(mMonitor);
+ aTrackData.mHighestStartTimestamp = aHighestTime;
+ }
+}
+
+uint32_t
+TrackBuffersManager::RemoveFrames(const TimeIntervals& aIntervals,
+ TrackData& aTrackData,
+ uint32_t aStartIndex)
+{
+ TrackBuffer& data = aTrackData.GetTrackBuffer();
+ Maybe<uint32_t> firstRemovedIndex;
+ uint32_t lastRemovedIndex = 0;
+
+ // We loop from aStartIndex to avoid removing frames that we inserted earlier
+ // and part of the current coded frame group. This is allows to handle step
+ // 14 of the coded frame processing algorithm without having to check the value
+ // of highest end timestamp:
+ // "Remove existing coded frames in track buffer:
+ // If highest end timestamp for track buffer is not set:
+ // Remove all coded frames from track buffer that have a presentation timestamp greater than or equal to presentation timestamp and less than frame end timestamp.
+ // If highest end timestamp for track buffer is set and less than or equal to presentation timestamp:
+ // Remove all coded frames from track buffer that have a presentation timestamp greater than or equal to highest end timestamp and less than frame end timestamp"
+ TimeUnit intervalsEnd = aIntervals.GetEnd();
+ bool mayBreakLoop = false;
+ for (uint32_t i = aStartIndex; i < data.Length(); i++) {
+ const RefPtr<MediaRawData> sample = data[i];
+ TimeInterval sampleInterval =
+ TimeInterval(TimeUnit::FromMicroseconds(sample->mTime),
+ TimeUnit::FromMicroseconds(sample->GetEndTime()));
+ if (aIntervals.Contains(sampleInterval)) {
+ if (firstRemovedIndex.isNothing()) {
+ firstRemovedIndex = Some(i);
+ }
+ lastRemovedIndex = i;
+ mayBreakLoop = false;
+ continue;
+ }
+ if (sample->mKeyframe && mayBreakLoop) {
+ break;
+ }
+ if (sampleInterval.mStart > intervalsEnd) {
+ mayBreakLoop = true;
+ }
+ }
+
+ if (firstRemovedIndex.isNothing()) {
+ return 0;
+ }
+
+ // Remove decoding dependencies of the coded frames removed in the previous step:
+ // Remove all coded frames between the coded frames removed in the previous step and the next random access point after those removed frames.
+ for (uint32_t i = lastRemovedIndex + 1; i < data.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = data[i];
+ if (sample->mKeyframe) {
+ break;
+ }
+ lastRemovedIndex = i;
+ }
+
+ int64_t maxSampleDuration = 0;
+ uint32_t sizeRemoved = 0;
+ TimeIntervals removedIntervals;
+ for (uint32_t i = firstRemovedIndex.ref(); i <= lastRemovedIndex; i++) {
+ const RefPtr<MediaRawData> sample = data[i];
+ TimeInterval sampleInterval =
+ TimeInterval(TimeUnit::FromMicroseconds(sample->mTime),
+ TimeUnit::FromMicroseconds(sample->GetEndTime()));
+ removedIntervals += sampleInterval;
+ if (sample->mDuration > maxSampleDuration) {
+ maxSampleDuration = sample->mDuration;
+ }
+ sizeRemoved += sample->ComputedSizeOfIncludingThis();
+ }
+ aTrackData.mSizeBuffer -= sizeRemoved;
+
+ MSE_DEBUG("Removing frames from:%u (frames:%u) ([%f, %f))",
+ firstRemovedIndex.ref(),
+ lastRemovedIndex - firstRemovedIndex.ref() + 1,
+ removedIntervals.GetStart().ToSeconds(),
+ removedIntervals.GetEnd().ToSeconds());
+
+ if (aTrackData.mNextGetSampleIndex.isSome()) {
+ if (aTrackData.mNextGetSampleIndex.ref() >= firstRemovedIndex.ref() &&
+ aTrackData.mNextGetSampleIndex.ref() <= lastRemovedIndex) {
+ MSE_DEBUG("Next sample to be played got evicted");
+ aTrackData.mNextGetSampleIndex.reset();
+ ResetEvictionIndex(aTrackData);
+ } else if (aTrackData.mNextGetSampleIndex.ref() > lastRemovedIndex) {
+ uint32_t samplesRemoved = lastRemovedIndex - firstRemovedIndex.ref() + 1;
+ aTrackData.mNextGetSampleIndex.ref() -= samplesRemoved;
+ if (aTrackData.mEvictionIndex.mLastIndex > lastRemovedIndex) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ aTrackData.mEvictionIndex.mLastIndex >= samplesRemoved &&
+ aTrackData.mEvictionIndex.mEvictable >= sizeRemoved,
+ "Invalid eviction index");
+ MonitorAutoLock mon(mMonitor);
+ aTrackData.mEvictionIndex.mLastIndex -= samplesRemoved;
+ aTrackData.mEvictionIndex.mEvictable -= sizeRemoved;
+ } else {
+ ResetEvictionIndex(aTrackData);
+ }
+ }
+ }
+
+ if (aTrackData.mNextInsertionIndex.isSome()) {
+ if (aTrackData.mNextInsertionIndex.ref() > firstRemovedIndex.ref() &&
+ aTrackData.mNextInsertionIndex.ref() <= lastRemovedIndex + 1) {
+ aTrackData.ResetAppendState();
+ MSE_DEBUG("NextInsertionIndex got reset.");
+ } else if (aTrackData.mNextInsertionIndex.ref() > lastRemovedIndex + 1) {
+ aTrackData.mNextInsertionIndex.ref() -=
+ lastRemovedIndex - firstRemovedIndex.ref() + 1;
+ }
+ }
+
+ // Update our buffered range to exclude the range just removed.
+ aTrackData.mBufferedRanges -= removedIntervals;
+
+ // Recalculate sanitized buffered ranges.
+ aTrackData.mSanitizedBufferedRanges = aTrackData.mBufferedRanges;
+ aTrackData.mSanitizedBufferedRanges.SetFuzz(TimeUnit::FromMicroseconds(maxSampleDuration/2));
+
+ data.RemoveElementsAt(firstRemovedIndex.ref(),
+ lastRemovedIndex - firstRemovedIndex.ref() + 1);
+
+ if (aIntervals.GetEnd() >= aTrackData.mHighestStartTimestamp) {
+ // The sample with the highest presentation time got removed.
+ // Rescan the trackbuffer to determine the new one.
+ int64_t highestStartTime = 0;
+ for (const auto& sample : data) {
+ if (sample->mTime > highestStartTime) {
+ highestStartTime = sample->mTime;
+ }
+ }
+ MonitorAutoLock mon(mMonitor);
+ aTrackData.mHighestStartTimestamp =
+ TimeUnit::FromMicroseconds(highestStartTime);
+ }
+
+ return firstRemovedIndex.ref();
+}
+
+void
+TrackBuffersManager::RecreateParser(bool aReuseInitData)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ // Recreate our parser for only the data remaining. This is required
+ // as it has parsed the entire InputBuffer provided.
+ // Once the old TrackBuffer/MediaSource implementation is removed
+ // we can optimize this part. TODO
+ mParser = ContainerParser::CreateForMIMEType(mType);
+ if (aReuseInitData && mInitData) {
+ int64_t start, end;
+ mParser->ParseStartAndEndTimestamps(mInitData, start, end);
+ mProcessedInput = mInitData->Length();
+ } else {
+ mProcessedInput = 0;
+ }
+}
+
+nsTArray<TrackBuffersManager::TrackData*>
+TrackBuffersManager::GetTracksList()
+{
+ nsTArray<TrackData*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoTracks);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioTracks);
+ }
+ return tracks;
+}
+
+nsTArray<const TrackBuffersManager::TrackData*>
+TrackBuffersManager::GetTracksList() const
+{
+ nsTArray<const TrackData*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoTracks);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioTracks);
+ }
+ return tracks;
+}
+
+void
+TrackBuffersManager::SetAppendState(SourceBufferAttributes::AppendState aAppendState)
+{
+ MSE_DEBUG("AppendState changed from %s to %s",
+ AppendStateToStr(mSourceBufferAttributes->GetAppendState()), AppendStateToStr(aAppendState));
+ mSourceBufferAttributes->SetAppendState(aAppendState);
+}
+
+MediaInfo
+TrackBuffersManager::GetMetadata() const
+{
+ MonitorAutoLock mon(mMonitor);
+ return mInfo;
+}
+
+const TimeIntervals&
+TrackBuffersManager::Buffered(TrackInfo::TrackType aTrack) const
+{
+ MOZ_ASSERT(OnTaskQueue());
+ return GetTracksData(aTrack).mBufferedRanges;
+}
+
+const media::TimeUnit&
+TrackBuffersManager::HighestStartTime(TrackInfo::TrackType aTrack) const
+{
+ MOZ_ASSERT(OnTaskQueue());
+ return GetTracksData(aTrack).mHighestStartTimestamp;
+}
+
+TimeIntervals
+TrackBuffersManager::SafeBuffered(TrackInfo::TrackType aTrack) const
+{
+ MonitorAutoLock mon(mMonitor);
+ return aTrack == TrackInfo::kVideoTrack
+ ? mVideoBufferedRanges
+ : mAudioBufferedRanges;
+}
+
+TimeUnit
+TrackBuffersManager::HighestStartTime() const
+{
+ MonitorAutoLock mon(mMonitor);
+ TimeUnit highestStartTime;
+ for (auto& track : GetTracksList()) {
+ highestStartTime =
+ std::max(track->mHighestStartTimestamp, highestStartTime);
+ }
+ return highestStartTime;
+}
+
+TimeUnit
+TrackBuffersManager::HighestEndTime() const
+{
+ MonitorAutoLock mon(mMonitor);
+
+ nsTArray<const TimeIntervals*> tracks;
+ if (HasVideo()) {
+ tracks.AppendElement(&mVideoBufferedRanges);
+ }
+ if (HasAudio()) {
+ tracks.AppendElement(&mAudioBufferedRanges);
+ }
+ return HighestEndTime(tracks);
+}
+
+TimeUnit
+TrackBuffersManager::HighestEndTime(
+ nsTArray<const TimeIntervals*>& aTracks) const
+{
+ mMonitor.AssertCurrentThreadOwns();
+
+ TimeUnit highestEndTime;
+
+ for (const auto& trackRanges : aTracks) {
+ highestEndTime = std::max(trackRanges->GetEnd(), highestEndTime);
+ }
+ return highestEndTime;
+}
+
+void
+TrackBuffersManager::ResetEvictionIndex(TrackData& aTrackData)
+{
+ MonitorAutoLock mon(mMonitor);
+ aTrackData.mEvictionIndex.Reset();
+}
+
+void
+TrackBuffersManager::UpdateEvictionIndex(TrackData& aTrackData,
+ uint32_t currentIndex)
+{
+ uint32_t evictable = 0;
+ TrackBuffer& data = aTrackData.GetTrackBuffer();
+ MOZ_DIAGNOSTIC_ASSERT(currentIndex >= aTrackData.mEvictionIndex.mLastIndex,
+ "Invalid call");
+ MOZ_DIAGNOSTIC_ASSERT(currentIndex == data.Length() ||
+ data[currentIndex]->mKeyframe,"Must stop at keyframe");
+
+ for (uint32_t i = aTrackData.mEvictionIndex.mLastIndex; i < currentIndex;
+ i++) {
+ evictable += data[i]->ComputedSizeOfIncludingThis();
+ }
+ aTrackData.mEvictionIndex.mLastIndex = currentIndex;
+ MonitorAutoLock mon(mMonitor);
+ aTrackData.mEvictionIndex.mEvictable += evictable;
+}
+
+const TrackBuffersManager::TrackBuffer&
+TrackBuffersManager::GetTrackBuffer(TrackInfo::TrackType aTrack) const
+{
+ MOZ_ASSERT(OnTaskQueue());
+ return GetTracksData(aTrack).GetTrackBuffer();
+}
+
+uint32_t TrackBuffersManager::FindSampleIndex(const TrackBuffer& aTrackBuffer,
+ const TimeInterval& aInterval)
+{
+ TimeUnit target = aInterval.mStart - aInterval.mFuzz;
+
+ for (uint32_t i = 0; i < aTrackBuffer.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = aTrackBuffer[i];
+ if (sample->mTime >= target.ToMicroseconds() ||
+ sample->GetEndTime() > target.ToMicroseconds()) {
+ return i;
+ }
+ }
+ NS_ASSERTION(false, "FindSampleIndex called with invalid arguments");
+
+ return 0;
+}
+
+TimeUnit
+TrackBuffersManager::Seek(TrackInfo::TrackType aTrack,
+ const TimeUnit& aTime,
+ const TimeUnit& aFuzz)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ auto& trackBuffer = GetTracksData(aTrack);
+ const TrackBuffersManager::TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ if (!track.Length()) {
+ // This a reset. It will be followed by another valid seek.
+ trackBuffer.mNextGetSampleIndex = Some(uint32_t(0));
+ trackBuffer.mNextSampleTimecode = TimeUnit();
+ trackBuffer.mNextSampleTime = TimeUnit();
+ ResetEvictionIndex(trackBuffer);
+ return TimeUnit();
+ }
+
+ uint32_t i = 0;
+
+ if (aTime != TimeUnit()) {
+ // Determine the interval of samples we're attempting to seek to.
+ TimeIntervals buffered = trackBuffer.mBufferedRanges;
+ // Fuzz factor is +/- aFuzz; as we want to only eliminate gaps
+ // that are less than aFuzz wide, we set a fuzz factor aFuzz/2.
+ buffered.SetFuzz(aFuzz / 2);
+ TimeIntervals::IndexType index = buffered.Find(aTime);
+ MOZ_ASSERT(index != TimeIntervals::NoIndex,
+ "We shouldn't be called if aTime isn't buffered");
+ TimeInterval target = buffered[index];
+ target.mFuzz = aFuzz;
+ i = FindSampleIndex(track, target);
+ }
+
+ Maybe<TimeUnit> lastKeyFrameTime;
+ TimeUnit lastKeyFrameTimecode;
+ uint32_t lastKeyFrameIndex = 0;
+ for (; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeUnit sampleTime = TimeUnit::FromMicroseconds(sample->mTime);
+ if (sampleTime > aTime && lastKeyFrameTime.isSome()) {
+ break;
+ }
+ if (sample->mKeyframe) {
+ lastKeyFrameTimecode = TimeUnit::FromMicroseconds(sample->mTimecode);
+ lastKeyFrameTime = Some(sampleTime);
+ lastKeyFrameIndex = i;
+ }
+ if (sampleTime == aTime ||
+ (sampleTime > aTime && lastKeyFrameTime.isSome())) {
+ break;
+ }
+ }
+ MSE_DEBUG("Keyframe %s found at %lld @ %u",
+ lastKeyFrameTime.isSome() ? "" : "not",
+ lastKeyFrameTime.refOr(TimeUnit()).ToMicroseconds(),
+ lastKeyFrameIndex);
+
+ trackBuffer.mNextGetSampleIndex = Some(lastKeyFrameIndex);
+ trackBuffer.mNextSampleTimecode = lastKeyFrameTimecode;
+ trackBuffer.mNextSampleTime = lastKeyFrameTime.refOr(TimeUnit());
+ ResetEvictionIndex(trackBuffer);
+ UpdateEvictionIndex(trackBuffer, lastKeyFrameIndex);
+
+ return lastKeyFrameTime.refOr(TimeUnit());
+}
+
+uint32_t
+TrackBuffersManager::SkipToNextRandomAccessPoint(TrackInfo::TrackType aTrack,
+ const TimeUnit& aTimeThreadshold,
+ const media::TimeUnit& aFuzz,
+ bool& aFound)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ uint32_t parsed = 0;
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+ aFound = false;
+
+ // SkipToNextRandomAccessPoint can only be called if aTimeThreadshold is known
+ // to be buffered.
+
+ // So first determine the current position in the track buffer if necessary.
+ if (trackData.mNextGetSampleIndex.isNothing()) {
+ if (trackData.mNextSampleTimecode == TimeUnit()) {
+ // First demux, get first sample.
+ trackData.mNextGetSampleIndex = Some(0u);
+ } else {
+ int32_t pos = FindCurrentPosition(aTrack, aFuzz);
+ if (pos < 0) {
+ return 0;
+ }
+ trackData.mNextGetSampleIndex = Some(uint32_t(pos));
+ }
+ }
+
+ TimeUnit nextSampleTimecode = trackData.mNextSampleTimecode;
+ TimeUnit nextSampleTime = trackData.mNextSampleTime;
+ uint32_t i = trackData.mNextGetSampleIndex.ref();
+ int32_t originalPos = i;
+
+ for (; i < track.Length(); i++) {
+ const MediaRawData* sample =
+ GetSample(aTrack,
+ i,
+ nextSampleTimecode,
+ nextSampleTime,
+ aFuzz);
+ if (!sample) {
+ break;
+ }
+ if (sample->mKeyframe &&
+ sample->mTime >= aTimeThreadshold.ToMicroseconds()) {
+ aFound = true;
+ break;
+ }
+ nextSampleTimecode =
+ TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration);
+ nextSampleTime = TimeUnit::FromMicroseconds(sample->GetEndTime());
+ parsed++;
+ }
+
+ // Adjust the next demux time and index so that the next call to
+ // SkipToNextRandomAccessPoint will not count again the parsed sample as
+ // skipped.
+ if (aFound) {
+ trackData.mNextSampleTimecode =
+ TimeUnit::FromMicroseconds(track[i]->mTimecode);
+ trackData.mNextSampleTime =
+ TimeUnit::FromMicroseconds(track[i]->mTime);
+ trackData.mNextGetSampleIndex = Some(i);
+ } else if (i > 0) {
+ // Go back to the previous keyframe or the original position so the next
+ // demux can succeed and be decoded.
+ for (int j = i - 1; j >= originalPos; j--) {
+ const RefPtr<MediaRawData>& sample = track[j];
+ if (sample->mKeyframe) {
+ trackData.mNextSampleTimecode =
+ TimeUnit::FromMicroseconds(sample->mTimecode);
+ trackData.mNextSampleTime = TimeUnit::FromMicroseconds(sample->mTime);
+ trackData.mNextGetSampleIndex = Some(uint32_t(j));
+ // We are unable to skip to a keyframe past aTimeThreshold, however
+ // we are speeding up decoding by dropping the unplayable frames.
+ // So we can mark aFound as true.
+ aFound = true;
+ break;
+ }
+ parsed--;
+ }
+ }
+
+ if (aFound) {
+ UpdateEvictionIndex(trackData, trackData.mNextGetSampleIndex.ref());
+ }
+
+ return parsed;
+}
+
+const MediaRawData*
+TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
+ uint32_t aIndex,
+ const TimeUnit& aExpectedDts,
+ const TimeUnit& aExpectedPts,
+ const TimeUnit& aFuzz)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ if (aIndex >= track.Length()) {
+ // reached the end.
+ return nullptr;
+ }
+
+ const RefPtr<MediaRawData>& sample = track[aIndex];
+ if (!aIndex || sample->mTimecode <= (aExpectedDts + aFuzz).ToMicroseconds() ||
+ sample->mTime <= (aExpectedPts + aFuzz).ToMicroseconds()) {
+ return sample;
+ }
+
+ // Gap is too big. End of Stream or Waiting for Data.
+ // TODO, check that we have continuous data based on the sanitized buffered
+ // range instead.
+ return nullptr;
+}
+
+already_AddRefed<MediaRawData>
+TrackBuffersManager::GetSample(TrackInfo::TrackType aTrack,
+ const TimeUnit& aFuzz,
+ MediaResult& aResult)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ aResult = NS_ERROR_DOM_MEDIA_WAITING_FOR_DATA;
+
+ if (!track.Length()) {
+ aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ return nullptr;
+ }
+
+ if (trackData.mNextGetSampleIndex.isNothing() &&
+ trackData.mNextSampleTimecode == TimeUnit()) {
+ // First demux, get first sample.
+ trackData.mNextGetSampleIndex = Some(0u);
+ }
+
+ if (trackData.mNextGetSampleIndex.isSome()) {
+ if (trackData.mNextGetSampleIndex.ref() >= track.Length()) {
+ aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ return nullptr;
+ }
+ const MediaRawData* sample =
+ GetSample(aTrack,
+ trackData.mNextGetSampleIndex.ref(),
+ trackData.mNextSampleTimecode,
+ trackData.mNextSampleTime,
+ aFuzz);
+ if (!sample) {
+ return nullptr;
+ }
+
+ RefPtr<MediaRawData> p = sample->Clone();
+ if (!p) {
+ aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return nullptr;
+ }
+ if (p->mKeyframe) {
+ UpdateEvictionIndex(trackData, trackData.mNextGetSampleIndex.ref());
+ }
+ trackData.mNextGetSampleIndex.ref()++;
+ // Estimate decode timestamp and timestamp of the next sample.
+ TimeUnit nextSampleTimecode =
+ TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration);
+ TimeUnit nextSampleTime =
+ TimeUnit::FromMicroseconds(sample->GetEndTime());
+ const MediaRawData* nextSample =
+ GetSample(aTrack,
+ trackData.mNextGetSampleIndex.ref(),
+ nextSampleTimecode,
+ nextSampleTime,
+ aFuzz);
+ if (nextSample) {
+ // We have a valid next sample, can use exact values.
+ trackData.mNextSampleTimecode =
+ TimeUnit::FromMicroseconds(nextSample->mTimecode);
+ trackData.mNextSampleTime =
+ TimeUnit::FromMicroseconds(nextSample->mTime);
+ } else {
+ // Next sample isn't available yet. Use estimates.
+ trackData.mNextSampleTimecode = nextSampleTimecode;
+ trackData.mNextSampleTime = nextSampleTime;
+ }
+ aResult = NS_OK;
+ return p.forget();
+ }
+
+ if (trackData.mNextSampleTimecode.ToMicroseconds() >
+ track.LastElement()->mTimecode + track.LastElement()->mDuration) {
+ // The next element is past our last sample. We're done.
+ trackData.mNextGetSampleIndex = Some(uint32_t(track.Length()));
+ aResult = NS_ERROR_DOM_MEDIA_END_OF_STREAM;
+ return nullptr;
+ }
+
+ // Our previous index has been overwritten, attempt to find the new one.
+ int32_t pos = FindCurrentPosition(aTrack, aFuzz);
+ if (pos < 0) {
+ MSE_DEBUG("Couldn't find sample (pts:%lld dts:%lld)",
+ trackData.mNextSampleTime.ToMicroseconds(),
+ trackData.mNextSampleTimecode.ToMicroseconds());
+ return nullptr;
+ }
+
+ const RefPtr<MediaRawData>& sample = track[pos];
+ RefPtr<MediaRawData> p = sample->Clone();
+ if (!p) {
+ // OOM
+ aResult = MediaResult(NS_ERROR_OUT_OF_MEMORY, __func__);
+ return nullptr;
+ }
+
+ // Find the previous keyframe to calculate the evictable amount.
+ int32_t i = pos;
+ for (; !track[i]->mKeyframe; i--) {
+ }
+ UpdateEvictionIndex(trackData, i);
+
+ trackData.mNextGetSampleIndex = Some(uint32_t(pos)+1);
+ trackData.mNextSampleTimecode =
+ TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration);
+ trackData.mNextSampleTime =
+ TimeUnit::FromMicroseconds(sample->GetEndTime());
+ aResult = NS_OK;
+ return p.forget();
+}
+
+int32_t
+TrackBuffersManager::FindCurrentPosition(TrackInfo::TrackType aTrack,
+ const TimeUnit& aFuzz) const
+{
+ MOZ_ASSERT(OnTaskQueue());
+ auto& trackData = GetTracksData(aTrack);
+ const TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ // Perform an exact search first.
+ for (uint32_t i = 0; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeInterval sampleInterval{
+ TimeUnit::FromMicroseconds(sample->mTimecode),
+ TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration)};
+
+ if (sampleInterval.ContainsStrict(trackData.mNextSampleTimecode)) {
+ return i;
+ }
+ if (sampleInterval.mStart > trackData.mNextSampleTimecode) {
+ // Samples are ordered by timecode. There's no need to search
+ // any further.
+ break;
+ }
+ }
+
+ for (uint32_t i = 0; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeInterval sampleInterval{
+ TimeUnit::FromMicroseconds(sample->mTimecode),
+ TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration),
+ aFuzz};
+
+ if (sampleInterval.ContainsWithStrictEnd(trackData.mNextSampleTimecode)) {
+ return i;
+ }
+ if (sampleInterval.mStart - aFuzz > trackData.mNextSampleTimecode) {
+ // Samples are ordered by timecode. There's no need to search
+ // any further.
+ break;
+ }
+ }
+
+ // We couldn't find our sample by decode timestamp. Attempt to find it using
+ // presentation timestamp. There will likely be small jerkiness.
+ for (uint32_t i = 0; i < track.Length(); i++) {
+ const RefPtr<MediaRawData>& sample = track[i];
+ TimeInterval sampleInterval{
+ TimeUnit::FromMicroseconds(sample->mTime),
+ TimeUnit::FromMicroseconds(sample->GetEndTime()),
+ aFuzz};
+
+ if (sampleInterval.ContainsWithStrictEnd(trackData.mNextSampleTimecode)) {
+ return i;
+ }
+ }
+
+ // Still not found.
+ return -1;
+}
+
+uint32_t
+TrackBuffersManager::Evictable(TrackInfo::TrackType aTrack) const
+{
+ MonitorAutoLock mon(mMonitor);
+ return GetTracksData(aTrack).mEvictionIndex.mEvictable;
+}
+
+TimeUnit
+TrackBuffersManager::GetNextRandomAccessPoint(TrackInfo::TrackType aTrack,
+ const TimeUnit& aFuzz)
+{
+ MOZ_ASSERT(OnTaskQueue());
+ auto& trackData = GetTracksData(aTrack);
+ MOZ_ASSERT(trackData.mNextGetSampleIndex.isSome());
+ const TrackBuffersManager::TrackBuffer& track = GetTrackBuffer(aTrack);
+
+ uint32_t i = trackData.mNextGetSampleIndex.ref();
+ TimeUnit nextSampleTimecode = trackData.mNextSampleTimecode;
+ TimeUnit nextSampleTime = trackData.mNextSampleTime;
+
+ for (; i < track.Length(); i++) {
+ const MediaRawData* sample =
+ GetSample(aTrack, i, nextSampleTimecode, nextSampleTime, aFuzz);
+ if (!sample) {
+ break;
+ }
+ if (sample->mKeyframe) {
+ return TimeUnit::FromMicroseconds(sample->mTime);
+ }
+ nextSampleTimecode =
+ TimeUnit::FromMicroseconds(sample->mTimecode + sample->mDuration);
+ nextSampleTime = TimeUnit::FromMicroseconds(sample->GetEndTime());
+ }
+ return TimeUnit::FromInfinity();
+}
+
+void
+TrackBuffersManager::TrackData::AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes) const
+{
+ for (const TrackBuffer& buffer : mBuffers) {
+ for (const MediaRawData* data : buffer) {
+ aSizes->mByteSize += data->SizeOfIncludingThis(aSizes->mMallocSizeOf);
+ }
+ }
+}
+
+void
+TrackBuffersManager::AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes) const
+{
+ MOZ_ASSERT(OnTaskQueue());
+ mVideoTracks.AddSizeOfResources(aSizes);
+ mAudioTracks.AddSizeOfResources(aSizes);
+}
+
+} // namespace mozilla
+#undef MSE_DEBUG
+#undef MSE_DEBUGV
+#undef SAMPLE_DEBUG
diff --git a/dom/media/mediasource/TrackBuffersManager.h b/dom/media/mediasource/TrackBuffersManager.h
new file mode 100644
index 000000000..de6b115f5
--- /dev/null
+++ b/dom/media/mediasource/TrackBuffersManager.h
@@ -0,0 +1,500 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef MOZILLA_TRACKBUFFERSMANAGER_H_
+#define MOZILLA_TRACKBUFFERSMANAGER_H_
+
+#include "mozilla/Atomics.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/Monitor.h"
+#include "AutoTaskQueue.h"
+#include "mozilla/dom/SourceBufferBinding.h"
+
+#include "MediaData.h"
+#include "MediaDataDemuxer.h"
+#include "MediaResult.h"
+#include "MediaSourceDecoder.h"
+#include "SourceBufferTask.h"
+#include "TimeUnits.h"
+#include "nsAutoPtr.h"
+#include "nsProxyRelease.h"
+#include "nsString.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+
+class ContainerParser;
+class MediaByteBuffer;
+class MediaRawData;
+class MediaSourceDemuxer;
+class SourceBufferResource;
+
+class SourceBufferTaskQueue
+{
+public:
+ SourceBufferTaskQueue()
+ : mMonitor("SourceBufferTaskQueue")
+ {}
+ ~SourceBufferTaskQueue()
+ {
+ MOZ_ASSERT(mQueue.IsEmpty(), "All tasks must have been processed");
+ }
+
+ void Push(SourceBufferTask* aTask)
+ {
+ MonitorAutoLock mon(mMonitor);
+ mQueue.AppendElement(aTask);
+ }
+
+ already_AddRefed<SourceBufferTask> Pop()
+ {
+ MonitorAutoLock mon(mMonitor);
+ if (!mQueue.Length()) {
+ return nullptr;
+ }
+ RefPtr<SourceBufferTask> task = Move(mQueue[0]);
+ mQueue.RemoveElementAt(0);
+ return task.forget();
+ }
+
+ nsTArray<SourceBufferTask>::size_type Length() const
+ {
+ MonitorAutoLock mon(mMonitor);
+ return mQueue.Length();
+ }
+
+private:
+ mutable Monitor mMonitor;
+ nsTArray<RefPtr<SourceBufferTask>> mQueue;
+};
+
+class TrackBuffersManager
+{
+public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(TrackBuffersManager);
+
+ enum class EvictDataResult : int8_t
+ {
+ NO_DATA_EVICTED,
+ CANT_EVICT,
+ BUFFER_FULL,
+ };
+
+ typedef TrackInfo::TrackType TrackType;
+ typedef MediaData::Type MediaType;
+ typedef nsTArray<RefPtr<MediaRawData>> TrackBuffer;
+ typedef SourceBufferTask::AppendPromise AppendPromise;
+ typedef SourceBufferTask::RangeRemovalPromise RangeRemovalPromise;
+
+ // Interface for SourceBuffer
+ TrackBuffersManager(MediaSourceDecoder* aParentDecoder,
+ const nsACString& aType);
+
+ // Queue a task to add data to the end of the input buffer and run the MSE
+ // Buffer Append Algorithm
+ // 3.5.5 Buffer Append Algorithm.
+ // http://w3c.github.io/media-source/index.html#sourcebuffer-buffer-append
+ RefPtr<AppendPromise> AppendData(MediaByteBuffer* aData,
+ const SourceBufferAttributes& aAttributes);
+
+ // Queue a task to abort any pending AppendData.
+ // Does nothing at this stage.
+ void AbortAppendData();
+
+ // Queue a task to run MSE Reset Parser State Algorithm.
+ // 3.5.2 Reset Parser State
+ void ResetParserState(SourceBufferAttributes& aAttributes);
+
+ // Queue a task to run the MSE range removal algorithm.
+ // http://w3c.github.io/media-source/#sourcebuffer-coded-frame-removal
+ RefPtr<RangeRemovalPromise> RangeRemoval(media::TimeUnit aStart,
+ media::TimeUnit aEnd);
+
+ // Schedule data eviction if necessary as the next call to AppendData will
+ // add aSize bytes.
+ // Eviction is done in two steps, first remove data up to aPlaybackTime
+ // and if still more space is needed remove from the end.
+ EvictDataResult EvictData(const media::TimeUnit& aPlaybackTime, int64_t aSize);
+
+ // Returns the buffered range currently managed.
+ // This may be called on any thread.
+ // Buffered must conform to http://w3c.github.io/media-source/index.html#widl-SourceBuffer-buffered
+ media::TimeIntervals Buffered() const;
+ media::TimeUnit HighestStartTime() const;
+ media::TimeUnit HighestEndTime() const;
+
+ // Return the size of the data managed by this SourceBufferContentManager.
+ int64_t GetSize() const;
+
+ // Indicate that the MediaSource parent object got into "ended" state.
+ void Ended();
+
+ // The parent SourceBuffer is about to be destroyed.
+ void Detach();
+
+ int64_t EvictionThreshold() const;
+
+ // Interface for MediaSourceDemuxer
+ MediaInfo GetMetadata() const;
+ const TrackBuffer& GetTrackBuffer(TrackInfo::TrackType aTrack) const;
+ const media::TimeIntervals& Buffered(TrackInfo::TrackType) const;
+ const media::TimeUnit& HighestStartTime(TrackInfo::TrackType) const;
+ media::TimeIntervals SafeBuffered(TrackInfo::TrackType) const;
+ bool IsEnded() const
+ {
+ return mEnded;
+ }
+ uint32_t Evictable(TrackInfo::TrackType aTrack) const;
+ media::TimeUnit Seek(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aTime,
+ const media::TimeUnit& aFuzz);
+ uint32_t SkipToNextRandomAccessPoint(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aTimeThreadshold,
+ const media::TimeUnit& aFuzz,
+ bool& aFound);
+
+ already_AddRefed<MediaRawData> GetSample(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz,
+ MediaResult& aResult);
+ int32_t FindCurrentPosition(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz) const;
+ media::TimeUnit GetNextRandomAccessPoint(TrackInfo::TrackType aTrack,
+ const media::TimeUnit& aFuzz);
+
+ void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes) const;
+
+private:
+ typedef MozPromise<bool, MediaResult, /* IsExclusive = */ true> CodedFrameProcessingPromise;
+
+ // for MediaSourceDemuxer::GetMozDebugReaderData
+ friend class MediaSourceDemuxer;
+ ~TrackBuffersManager();
+ // All following functions run on the taskqueue.
+ RefPtr<AppendPromise> DoAppendData(RefPtr<MediaByteBuffer> aData,
+ SourceBufferAttributes aAttributes);
+ void ScheduleSegmentParserLoop();
+ void SegmentParserLoop();
+ void InitializationSegmentReceived();
+ void ShutdownDemuxers();
+ void CreateDemuxerforMIMEType();
+ void ResetDemuxingState();
+ void NeedMoreData();
+ void RejectAppend(const MediaResult& aRejectValue, const char* aName);
+ // Will return a promise that will be resolved once all frames of the current
+ // media segment have been processed.
+ RefPtr<CodedFrameProcessingPromise> CodedFrameProcessing();
+ void CompleteCodedFrameProcessing();
+ // Called by ResetParserState.
+ void CompleteResetParserState();
+ RefPtr<RangeRemovalPromise>
+ CodedFrameRemovalWithPromise(media::TimeInterval aInterval);
+ bool CodedFrameRemoval(media::TimeInterval aInterval);
+ void SetAppendState(SourceBufferAttributes::AppendState aAppendState);
+
+ bool HasVideo() const
+ {
+ return mVideoTracks.mNumTracks > 0;
+ }
+ bool HasAudio() const
+ {
+ return mAudioTracks.mNumTracks > 0;
+ }
+
+ // The input buffer as per http://w3c.github.io/media-source/index.html#sourcebuffer-input-buffer
+ RefPtr<MediaByteBuffer> mInputBuffer;
+ // Buffer full flag as per https://w3c.github.io/media-source/#sourcebuffer-buffer-full-flag.
+ // Accessed on both the main thread and the task queue.
+ Atomic<bool> mBufferFull;
+ bool mFirstInitializationSegmentReceived;
+ // Set to true once a new segment is started.
+ bool mNewMediaSegmentStarted;
+ bool mActiveTrack;
+ nsCString mType;
+
+ // ContainerParser objects and methods.
+ // Those are used to parse the incoming input buffer.
+
+ // Recreate the ContainerParser and if aReuseInitData is true then
+ // feed it with the previous init segment found.
+ void RecreateParser(bool aReuseInitData);
+ nsAutoPtr<ContainerParser> mParser;
+
+ // Demuxer objects and methods.
+ void AppendDataToCurrentInputBuffer(MediaByteBuffer* aData);
+ RefPtr<MediaByteBuffer> mInitData;
+ // Temporary input buffer to handle partial media segment header.
+ // We store the current input buffer content into it should we need to
+ // reinitialize the demuxer once we have some samples and a discontinuity is
+ // detected.
+ RefPtr<MediaByteBuffer> mPendingInputBuffer;
+ RefPtr<SourceBufferResource> mCurrentInputBuffer;
+ RefPtr<MediaDataDemuxer> mInputDemuxer;
+ // Length already processed in current media segment.
+ uint64_t mProcessedInput;
+ Maybe<media::TimeUnit> mLastParsedEndTime;
+
+ void OnDemuxerInitDone(nsresult);
+ void OnDemuxerInitFailed(const MediaResult& aFailure);
+ void OnDemuxerResetDone(nsresult);
+ MozPromiseRequestHolder<MediaDataDemuxer::InitPromise> mDemuxerInitRequest;
+
+ void OnDemuxFailed(TrackType aTrack, const MediaResult& aError);
+ void DoDemuxVideo();
+ void OnVideoDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ void OnVideoDemuxFailed(const MediaResult& aError)
+ {
+ mVideoTracks.mDemuxRequest.Complete();
+ OnDemuxFailed(TrackType::kVideoTrack, aError);
+ }
+ void DoDemuxAudio();
+ void OnAudioDemuxCompleted(RefPtr<MediaTrackDemuxer::SamplesHolder> aSamples);
+ void OnAudioDemuxFailed(const MediaResult& aError)
+ {
+ mAudioTracks.mDemuxRequest.Complete();
+ OnDemuxFailed(TrackType::kAudioTrack, aError);
+ }
+
+ void DoEvictData(const media::TimeUnit& aPlaybackTime, int64_t aSizeToEvict);
+
+ struct TrackData
+ {
+ TrackData()
+ : mNumTracks(0)
+ , mNeedRandomAccessPoint(true)
+ , mSizeBuffer(0)
+ {}
+ uint32_t mNumTracks;
+ // Definition of variables:
+ // https://w3c.github.io/media-source/#track-buffers
+ // Last decode timestamp variable that stores the decode timestamp of the
+ // last coded frame appended in the current coded frame group.
+ // The variable is initially unset to indicate that no coded frames have
+ // been appended yet.
+ Maybe<media::TimeUnit> mLastDecodeTimestamp;
+ // Last frame duration variable that stores the coded frame duration of the
+ // last coded frame appended in the current coded frame group.
+ // The variable is initially unset to indicate that no coded frames have
+ // been appended yet.
+ Maybe<media::TimeUnit> mLastFrameDuration;
+ // Highest end timestamp variable that stores the highest coded frame end
+ // timestamp across all coded frames in the current coded frame group that
+ // were appended to this track buffer.
+ // The variable is initially unset to indicate that no coded frames have
+ // been appended yet.
+ Maybe<media::TimeUnit> mHighestEndTimestamp;
+ // Highest presentation timestamp in track buffer.
+ // Protected by global monitor, except when reading on the task queue as it
+ // is only written there.
+ media::TimeUnit mHighestStartTimestamp;
+ // Longest frame duration seen since last random access point.
+ // Only ever accessed when mLastDecodeTimestamp and mLastFrameDuration are
+ // set.
+ media::TimeUnit mLongestFrameDuration;
+ // Need random access point flag variable that keeps track of whether the
+ // track buffer is waiting for a random access point coded frame.
+ // The variable is initially set to true to indicate that random access
+ // point coded frame is needed before anything can be added to the track
+ // buffer.
+ bool mNeedRandomAccessPoint;
+ RefPtr<MediaTrackDemuxer> mDemuxer;
+ MozPromiseRequestHolder<MediaTrackDemuxer::SamplesPromise> mDemuxRequest;
+ // Highest end timestamp of the last media segment demuxed.
+ media::TimeUnit mLastParsedEndTime;
+
+ // If set, position where the next contiguous frame will be inserted.
+ // If a discontinuity is detected, it will be unset and recalculated upon
+ // the next insertion.
+ Maybe<uint32_t> mNextInsertionIndex;
+ // Samples just demuxed, but not yet parsed.
+ TrackBuffer mQueuedSamples;
+ const TrackBuffer& GetTrackBuffer() const
+ {
+ MOZ_RELEASE_ASSERT(mBuffers.Length(),
+ "TrackBuffer must have been created");
+ return mBuffers.LastElement();
+ }
+ TrackBuffer& GetTrackBuffer()
+ {
+ MOZ_RELEASE_ASSERT(mBuffers.Length(),
+ "TrackBuffer must have been created");
+ return mBuffers.LastElement();
+ }
+ // We only manage a single track of each type at this time.
+ nsTArray<TrackBuffer> mBuffers;
+ // Track buffer ranges variable that represents the presentation time ranges
+ // occupied by the coded frames currently stored in the track buffer.
+ media::TimeIntervals mBufferedRanges;
+ // Sanitized mBufferedRanges with a fuzz of half a sample's duration applied
+ // This buffered ranges is the basis of what is exposed to the JS.
+ media::TimeIntervals mSanitizedBufferedRanges;
+ // Byte size of all samples contained in this track buffer.
+ uint32_t mSizeBuffer;
+ // TrackInfo of the first metadata received.
+ RefPtr<SharedTrackInfo> mInfo;
+ // TrackInfo of the last metadata parsed (updated with each init segment.
+ RefPtr<SharedTrackInfo> mLastInfo;
+
+ // If set, position of the next sample to be retrieved by GetSample().
+ // If the position is equal to the TrackBuffer's length, it indicates that
+ // we've reached EOS.
+ Maybe<uint32_t> mNextGetSampleIndex;
+ // Approximation of the next sample's decode timestamp.
+ media::TimeUnit mNextSampleTimecode;
+ // Approximation of the next sample's presentation timestamp.
+ media::TimeUnit mNextSampleTime;
+
+ struct EvictionIndex
+ {
+ EvictionIndex() { Reset(); }
+ void Reset()
+ {
+ mEvictable = 0;
+ mLastIndex = 0;
+ }
+ uint32_t mEvictable;
+ uint32_t mLastIndex;
+ };
+ // Size of data that can be safely evicted during the next eviction
+ // cycle.
+ // We consider as evictable all frames up to the last keyframe prior to
+ // mNextGetSampleIndex. If mNextGetSampleIndex isn't set, then we assume
+ // that we can't yet evict data.
+ // Protected by global monitor, except when reading on the task queue as it
+ // is only written there.
+ EvictionIndex mEvictionIndex;
+
+ void ResetAppendState()
+ {
+ mLastDecodeTimestamp.reset();
+ mLastFrameDuration.reset();
+ mHighestEndTimestamp.reset();
+ mNeedRandomAccessPoint = true;
+
+ mNextInsertionIndex.reset();
+ }
+
+ void AddSizeOfResources(MediaSourceDecoder::ResourceSizes* aSizes) const;
+ };
+
+ void CheckSequenceDiscontinuity(const media::TimeUnit& aPresentationTime);
+ void ProcessFrames(TrackBuffer& aSamples, TrackData& aTrackData);
+ media::TimeInterval PresentationInterval(const TrackBuffer& aSamples) const;
+ bool CheckNextInsertionIndex(TrackData& aTrackData,
+ const media::TimeUnit& aSampleTime);
+ void InsertFrames(TrackBuffer& aSamples,
+ const media::TimeIntervals& aIntervals,
+ TrackData& aTrackData);
+ void UpdateHighestTimestamp(TrackData& aTrackData,
+ const media::TimeUnit& aHighestTime);
+ // Remove all frames and their dependencies contained in aIntervals.
+ // Return the index at which frames were first removed or 0 if no frames
+ // removed.
+ uint32_t RemoveFrames(const media::TimeIntervals& aIntervals,
+ TrackData& aTrackData,
+ uint32_t aStartIndex);
+ // Recalculate track's evictable amount.
+ void ResetEvictionIndex(TrackData& aTrackData);
+ void UpdateEvictionIndex(TrackData& aTrackData, uint32_t aCurrentIndex);
+ // Find index of sample. Return a negative value if not found.
+ uint32_t FindSampleIndex(const TrackBuffer& aTrackBuffer,
+ const media::TimeInterval& aInterval);
+ const MediaRawData* GetSample(TrackInfo::TrackType aTrack,
+ uint32_t aIndex,
+ const media::TimeUnit& aExpectedDts,
+ const media::TimeUnit& aExpectedPts,
+ const media::TimeUnit& aFuzz);
+ void UpdateBufferedRanges();
+ void RejectProcessing(const MediaResult& aRejectValue, const char* aName);
+ void ResolveProcessing(bool aResolveValue, const char* aName);
+ MozPromiseRequestHolder<CodedFrameProcessingPromise> mProcessingRequest;
+ MozPromiseHolder<CodedFrameProcessingPromise> mProcessingPromise;
+
+ // Trackbuffers definition.
+ nsTArray<const TrackData*> GetTracksList() const;
+ nsTArray<TrackData*> GetTracksList();
+ TrackData& GetTracksData(TrackType aTrack)
+ {
+ switch(aTrack) {
+ case TrackType::kVideoTrack:
+ return mVideoTracks;
+ case TrackType::kAudioTrack:
+ default:
+ return mAudioTracks;
+ }
+ }
+ const TrackData& GetTracksData(TrackType aTrack) const
+ {
+ switch(aTrack) {
+ case TrackType::kVideoTrack:
+ return mVideoTracks;
+ case TrackType::kAudioTrack:
+ default:
+ return mAudioTracks;
+ }
+ }
+ TrackData mVideoTracks;
+ TrackData mAudioTracks;
+
+ // TaskQueue methods and objects.
+ AbstractThread* GetTaskQueue() const
+ {
+ return mTaskQueue;
+ }
+ bool OnTaskQueue() const
+ {
+ return !GetTaskQueue() || GetTaskQueue()->IsCurrentThreadIn();
+ }
+ RefPtr<AutoTaskQueue> mTaskQueue;
+
+ // SourceBuffer Queues and running context.
+ SourceBufferTaskQueue mQueue;
+ void QueueTask(SourceBufferTask* aTask);
+ void ProcessTasks();
+ // Set if the TrackBuffersManager is currently processing a task.
+ // At this stage, this task is always a AppendBufferTask.
+ RefPtr<SourceBufferTask> mCurrentTask;
+ // Current SourceBuffer state for ongoing task.
+ // Its content is returned to the SourceBuffer once the AppendBufferTask has
+ // completed.
+ UniquePtr<SourceBufferAttributes> mSourceBufferAttributes;
+ // The current sourcebuffer append window. It's content is equivalent to
+ // mSourceBufferAttributes.mAppendWindowStart/End
+ media::TimeInterval mAppendWindow;
+
+ // Strong references to external objects.
+ nsMainThreadPtrHandle<MediaSourceDecoder> mParentDecoder;
+
+ // Return public highest end time across all aTracks.
+ // Monitor must be held.
+ media::TimeUnit HighestEndTime(nsTArray<const media::TimeIntervals*>& aTracks) const;
+
+ // Set to true if mediasource state changed to ended.
+ Atomic<bool> mEnded;
+
+ // Global size of this source buffer content.
+ Atomic<int64_t> mSizeSourceBuffer;
+ const int64_t mVideoEvictionThreshold;
+ const int64_t mAudioEvictionThreshold;
+ enum class EvictionState
+ {
+ NO_EVICTION_NEEDED,
+ EVICTION_NEEDED,
+ EVICTION_COMPLETED,
+ };
+ Atomic<EvictionState> mEvictionState;
+
+ // Monitor to protect following objects accessed across multiple threads.
+ mutable Monitor mMonitor;
+ // Stable audio and video track time ranges.
+ media::TimeIntervals mVideoBufferedRanges;
+ media::TimeIntervals mAudioBufferedRanges;
+ // MediaInfo of the first init segment read.
+ MediaInfo mInfo;
+};
+
+} // namespace mozilla
+
+#endif /* MOZILLA_TRACKBUFFERSMANAGER_H_ */
diff --git a/dom/media/mediasource/gtest/TestContainerParser.cpp b/dom/media/mediasource/gtest/TestContainerParser.cpp
new file mode 100644
index 000000000..d51da83af
--- /dev/null
+++ b/dom/media/mediasource/gtest/TestContainerParser.cpp
@@ -0,0 +1,92 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <gtest/gtest.h>
+#include <stdint.h>
+
+#include "ContainerParser.h"
+#include "mozilla/ArrayUtils.h"
+#include "nsAutoPtr.h"
+
+using namespace mozilla;
+
+TEST(ContainerParser, MIMETypes) {
+ const char* content_types[] = {
+ "video/webm",
+ "audio/webm",
+ "video/mp4",
+ "audio/mp4",
+ "audio/aac"
+ };
+ nsAutoPtr<ContainerParser> parser;
+ for (size_t i = 0; i < ArrayLength(content_types); ++i) {
+ nsAutoCString content_type(content_types[i]);
+ parser = ContainerParser::CreateForMIMEType(content_type);
+ ASSERT_NE(parser, nullptr);
+ }
+}
+
+
+already_AddRefed<MediaByteBuffer> make_adts_header()
+{
+ const uint8_t test[] = { 0xff, 0xf1, 0x50, 0x80, 0x03, 0x1f, 0xfc };
+ RefPtr<MediaByteBuffer> buffer(new MediaByteBuffer);
+ buffer->AppendElements(test, ArrayLength(test));
+ return buffer.forget();
+}
+
+TEST(ContainerParser, ADTSHeader) {
+ nsAutoPtr<ContainerParser> parser;
+ parser = ContainerParser::CreateForMIMEType(NS_LITERAL_CSTRING("audio/aac"));
+ ASSERT_NE(parser, nullptr);
+
+ // Audio data should have no gaps.
+ EXPECT_EQ(parser->GetRoundingError(), 0);
+
+ // Test a valid header.
+ RefPtr<MediaByteBuffer> header = make_adts_header();
+ EXPECT_TRUE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)));
+
+ // Test variations.
+ uint8_t save = header->ElementAt(1);
+ for (uint8_t i = 1; i < 3; ++i) {
+ // Set non-zero layer.
+ header->ReplaceElementAt(1, (header->ElementAt(1) & 0xf9) | (i << 1));
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)))
+ << "Accepted non-zero layer in header.";
+ }
+ header->ReplaceElementAt(1, save);
+ save = header->ElementAt(2);
+ header->ReplaceElementAt(2, (header->ElementAt(2) & 0x3b) | (15 << 2));
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)))
+ << "Accepted explicit frequency in header.";
+ header->ReplaceElementAt(2, save);
+
+ // Test a short header.
+ header->SetLength(6);
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsInitSegmentPresent(header)))
+ << "Accepted too-short header.";
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(header)))
+ << "Found media segment when there was just a partial header.";
+
+ // Test parse results.
+ header = make_adts_header();
+ EXPECT_FALSE(NS_SUCCEEDED(parser->IsMediaSegmentPresent(header)))
+ << "Found media segment when there was just a header.";
+ int64_t start = 0;
+ int64_t end = 0;
+ EXPECT_TRUE(NS_FAILED(parser->ParseStartAndEndTimestamps(header, start, end)));
+
+ EXPECT_TRUE(parser->HasInitData());
+ EXPECT_TRUE(parser->HasCompleteInitData());
+ MediaByteBuffer* init = parser->InitData();
+ ASSERT_NE(init, nullptr);
+ EXPECT_EQ(init->Length(), header->Length());
+
+ EXPECT_EQ(parser->InitSegmentRange(), MediaByteRange(0, int64_t(header->Length())));
+ // Media segment range should be empty here.
+ EXPECT_EQ(parser->MediaHeaderRange(), MediaByteRange());
+ EXPECT_EQ(parser->MediaSegmentRange(), MediaByteRange());
+}
diff --git a/dom/media/mediasource/gtest/moz.build b/dom/media/mediasource/gtest/moz.build
new file mode 100644
index 000000000..5aa597d8d
--- /dev/null
+++ b/dom/media/mediasource/gtest/moz.build
@@ -0,0 +1,16 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+ 'TestContainerParser.cpp',
+]
+
+LOCAL_INCLUDES += [
+ '/dom/media',
+ '/dom/media/mediasource',
+]
+
+FINAL_LIBRARY = 'xul-gtest'
diff --git a/dom/media/mediasource/moz.build b/dom/media/mediasource/moz.build
new file mode 100644
index 000000000..6ded1875d
--- /dev/null
+++ b/dom/media/mediasource/moz.build
@@ -0,0 +1,49 @@
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+MOCHITEST_MANIFESTS += ['test/mochitest.ini']
+
+EXPORTS += [
+ 'AsyncEventRunner.h',
+ 'AutoTaskQueue.h',
+ 'MediaSourceDecoder.h',
+ 'MediaSourceDemuxer.h',
+ 'SourceBufferAttributes.h',
+ 'SourceBufferTask.h',
+ 'TrackBuffersManager.h',
+]
+
+EXPORTS.mozilla.dom += [
+ 'MediaSource.h',
+ 'SourceBuffer.h',
+ 'SourceBufferList.h',
+]
+
+UNIFIED_SOURCES += [
+ 'ContainerParser.cpp',
+ 'MediaSource.cpp',
+ 'MediaSourceDecoder.cpp',
+ 'MediaSourceDemuxer.cpp',
+ 'MediaSourceUtils.cpp',
+ 'ResourceQueue.cpp',
+ 'SourceBuffer.cpp',
+ 'SourceBufferList.cpp',
+ 'SourceBufferResource.cpp',
+ 'TrackBuffersManager.cpp',
+]
+
+TEST_DIRS += [
+ 'gtest',
+]
+
+if CONFIG['MOZ_GONK_MEDIACODEC']:
+ DEFINES['MOZ_GONK_MEDIACODEC'] = True
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'
+
+if CONFIG['GNU_CXX']:
+ CXXFLAGS += ['-Wno-error=shadow']
diff --git a/dom/media/mediasource/test/aac20-48000-64000-1.m4s b/dom/media/mediasource/test/aac20-48000-64000-1.m4s
new file mode 100644
index 000000000..56506e1f2
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^ b/dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac20-48000-64000-2.m4s b/dom/media/mediasource/test/aac20-48000-64000-2.m4s
new file mode 100644
index 000000000..3faff17eb
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^ b/dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac20-48000-64000-init.mp4 b/dom/media/mediasource/test/aac20-48000-64000-init.mp4
new file mode 100644
index 000000000..b70e01651
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^ b/dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/aac20-48000-64000-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac51-48000-128000-1.m4s b/dom/media/mediasource/test/aac51-48000-128000-1.m4s
new file mode 100644
index 000000000..3424acfec
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^ b/dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac51-48000-128000-2.m4s b/dom/media/mediasource/test/aac51-48000-128000-2.m4s
new file mode 100644
index 000000000..b02bfd043
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^ b/dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/aac51-48000-128000-init.mp4 b/dom/media/mediasource/test/aac51-48000-128000-init.mp4
new file mode 100644
index 000000000..7d62401f2
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-init.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^ b/dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/aac51-48000-128000-init.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop1.m4s b/dom/media/mediasource/test/bipbop/bipbop1.m4s
new file mode 100644
index 000000000..a237f2e91
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop10.m4s b/dom/media/mediasource/test/bipbop/bipbop10.m4s
new file mode 100644
index 000000000..d1f5e6a0b
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop10.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop10.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop11.m4s b/dom/media/mediasource/test/bipbop/bipbop11.m4s
new file mode 100644
index 000000000..57232fb35
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop11.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop11.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop12.m4s b/dom/media/mediasource/test/bipbop/bipbop12.m4s
new file mode 100644
index 000000000..f9b18713e
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop12.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop12.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop13.m4s b/dom/media/mediasource/test/bipbop/bipbop13.m4s
new file mode 100644
index 000000000..f2a876946
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop13.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop13.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop2.m4s b/dom/media/mediasource/test/bipbop/bipbop2.m4s
new file mode 100644
index 000000000..baa0d8578
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop2s.mp4 b/dom/media/mediasource/test/bipbop/bipbop2s.mp4
new file mode 100644
index 000000000..4fd8b9cb6
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2s.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop2s.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop3.m4s b/dom/media/mediasource/test/bipbop/bipbop3.m4s
new file mode 100644
index 000000000..ed313e668
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop3.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop4.m4s b/dom/media/mediasource/test/bipbop/bipbop4.m4s
new file mode 100644
index 000000000..7709ac08c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop4.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop5.m4s b/dom/media/mediasource/test/bipbop/bipbop5.m4s
new file mode 100644
index 000000000..6d36788e4
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop5.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop5.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop6.m4s b/dom/media/mediasource/test/bipbop/bipbop6.m4s
new file mode 100644
index 000000000..64f475c70
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop6.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop6.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop7.m4s b/dom/media/mediasource/test/bipbop/bipbop7.m4s
new file mode 100644
index 000000000..c148918d6
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop7.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop7.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop8.m4s b/dom/media/mediasource/test/bipbop/bipbop8.m4s
new file mode 100644
index 000000000..707dd4848
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop8.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop8.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop9.m4s b/dom/media/mediasource/test/bipbop/bipbop9.m4s
new file mode 100644
index 000000000..538cf72a4
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop9.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop9.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s
new file mode 100755
index 000000000..3dad336e8
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s
new file mode 100755
index 000000000..dd7491241
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-video2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4 b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4
new file mode 100755
index 000000000..b1a2d4405
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_480_624kbps-videoinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s
new file mode 100644
index 000000000..33da98b5a
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s
new file mode 100644
index 000000000..36a98afd2
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio10.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s
new file mode 100644
index 000000000..23d4aa8d8
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio11.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s
new file mode 100644
index 000000000..96f4bcc34
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s
new file mode 100644
index 000000000..7de4bd0ca
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s
new file mode 100644
index 000000000..494c71eb9
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s
new file mode 100644
index 000000000..b50496b6c
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio5.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s
new file mode 100644
index 000000000..02cf4d363
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio6.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s
new file mode 100644
index 000000000..bb2252889
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio7.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s
new file mode 100644
index 000000000..04a6a7af9
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio8.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s
new file mode 100644
index 000000000..cb94b529a
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audio9.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4 b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4
new file mode 100644
index 000000000..bbf272197
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_audioinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_dash.mpd b/dom/media/mediasource/test/bipbop/bipbop_dash.mpd
new file mode 100644
index 000000000..532cdc65d
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_dash.mpd
@@ -0,0 +1,48 @@
+<?xml version="1.0"?>
+<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" minBufferTime="PT1.500000S" type="static" mediaPresentationDuration="PT0H0M9.98S" profiles="urn:mpeg:dash:profile:full:2011">
+ <ProgramInformation moreInformationURL="http://gpac.sourceforge.net">
+ <Title>bipbop_dash.mpd handcrafted by JYA</Title>
+ </ProgramInformation>
+
+ <Period duration="PT0H0M9.98S">
+ <AdaptationSet segmentAlignment="true" maxWidth="400" maxHeight="300" maxFrameRate="90000" par="4:3" lang="und">
+ <Representation id="1" mimeType="video/mp4" codecs="avc1.4d4015" width="400" height="300" frameRate="90000" sar="1:1" startWithSAP="1" bandwidth="226425">
+ <SegmentList timescale="90000" duration="69043">
+ <Initialization sourceURL="bipbop_videoinit.mp4"/>
+ <SegmentURL media="bipbop_video1.m4s"/>
+ <SegmentURL media="bipbop_video2.m4s"/>
+ <SegmentURL media="bipbop_video3.m4s"/>
+ <SegmentURL media="bipbop_video4.m4s"/>
+ <SegmentURL media="bipbop_video5.m4s"/>
+ <SegmentURL media="bipbop_video6.m4s"/>
+ <SegmentURL media="bipbop_video7.m4s"/>
+ <SegmentURL media="bipbop_video8.m4s"/>
+ <SegmentURL media="bipbop_video9.m4s"/>
+ <SegmentURL media="bipbop_video10.m4s"/>
+ <SegmentURL media="bipbop_video11.m4s"/>
+ <SegmentURL media="bipbop_video12.m4s"/>
+ <SegmentURL media="bipbop_video13.m4s"/>
+ </SegmentList>
+ </Representation>
+ </AdaptationSet>
+ <AdaptationSet segmentAlignment="true" lang="und">
+ <Representation id="1" mimeType="audio/mp4" codecs="mp4a.40.2" audioSamplingRate="22050" startWithSAP="1" bandwidth="7206">
+ <AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
+ <SegmentList timescale="22050" duration="20101">
+ <Initialization sourceURL="bipbop_audioinit.mp4"/>
+ <SegmentURL media="bipbop_audio1.m4s"/>
+ <SegmentURL media="bipbop_audio2.m4s"/>
+ <SegmentURL media="bipbop_audio3.m4s"/>
+ <SegmentURL media="bipbop_audio4.m4s"/>
+ <SegmentURL media="bipbop_audio5.m4s"/>
+ <SegmentURL media="bipbop_audio6.m4s"/>
+ <SegmentURL media="bipbop_audio7.m4s"/>
+ <SegmentURL media="bipbop_audio8.m4s"/>
+ <SegmentURL media="bipbop_audio9.m4s"/>
+ <SegmentURL media="bipbop_audio10.m4s"/>
+ <SegmentURL media="bipbop_audio11.m4s"/>
+ </SegmentList>
+ </Representation>
+ </AdaptationSet>
+ </Period>
+</MPD>
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video1.m4s b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s
new file mode 100644
index 000000000..929118251
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video1.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video10.m4s b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s
new file mode 100644
index 000000000..72c7afaca
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video10.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video11.m4s b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s
new file mode 100644
index 000000000..e6109f5e7
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video11.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video12.m4s b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s
new file mode 100644
index 000000000..5c54a510f
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video12.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video13.m4s b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s
new file mode 100644
index 000000000..c64f38a33
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video13.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video2.m4s b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s
new file mode 100644
index 000000000..cd34fae56
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video2.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video3.m4s b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s
new file mode 100644
index 000000000..5a1334004
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video3.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video4.m4s b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s
new file mode 100644
index 000000000..e8d96b6ed
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video4.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video5.m4s b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s
new file mode 100644
index 000000000..ca6a82046
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video5.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video6.m4s b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s
new file mode 100644
index 000000000..fe9824355
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video6.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video7.m4s b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s
new file mode 100644
index 000000000..3351fa685
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video7.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video8.m4s b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s
new file mode 100644
index 000000000..af26ae5f9
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video8.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video9.m4s b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s
new file mode 100644
index 000000000..25be672c1
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^ b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_video9.m4s^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4 b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4
new file mode 100644
index 000000000..7c9c533c3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbop_videoinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/bipbop/bipbopinit.mp4 b/dom/media/mediasource/test/bipbop/bipbopinit.mp4
new file mode 100644
index 000000000..39f0575a7
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbopinit.mp4
Binary files differ
diff --git a/dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^ b/dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/bipbop/bipbopinit.mp4^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/crashtests/1005366.html b/dom/media/mediasource/test/crashtests/1005366.html
new file mode 100644
index 000000000..aa8b7f652
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/1005366.html
@@ -0,0 +1,27 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+/*
+user_pref("media.mediasource.enabled", true);
+*/
+
+function boom()
+{
+ var source = new window.MediaSource();
+ var videoElement = document.createElementNS('http://www.w3.org/1999/xhtml', 'video');
+ videoElement.src = URL.createObjectURL(source);
+
+ setTimeout(function() {
+ var buf = source.addSourceBuffer("video/webm");
+ buf.abort();
+ buf.appendBuffer(new Float32Array(203));
+ }, 0);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/1059035.html b/dom/media/mediasource/test/crashtests/1059035.html
new file mode 100644
index 000000000..9dfda34b8
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/1059035.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html>
+<html>
+<head>
+<script>
+
+/*
+user_pref("media.mediasource.enabled", true);
+*/
+
+function boom()
+{
+ var mediaSource = new MediaSource();
+ var htmlAudio = document.createElement("audio");
+ htmlAudio.src = URL.createObjectURL(mediaSource);
+
+ setTimeout(function() {
+ var sourceBuffer = mediaSource.addSourceBuffer("video/webm");
+ mediaSource.removeSourceBuffer(sourceBuffer);
+ sourceBuffer.remove(0, 0);
+ }, 0);
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/926665.html b/dom/media/mediasource/test/crashtests/926665.html
new file mode 100644
index 000000000..a8247b27e
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/926665.html
@@ -0,0 +1,26 @@
+<html>
+<head>
+<meta charset="UTF-8">
+<script style="display: none;" id="fuzz1" type="text/javascript;version=1.7">
+
+function boom()
+{
+ var mediaSource = new window.MediaSource();
+ var mediaSourceURL = URL.createObjectURL(mediaSource);
+ var v1 = document.createElement('video');
+ v1.src = mediaSourceURL;
+ mediaSource.addEventListener("sourceopen", function (e) {
+ var v2 = document.createElement('video');
+ v2.src = mediaSourceURL;
+ setTimeout(function () {
+ v2.src = "data:text/plain,1";
+ v1.src = "data:text/plain,2";
+ }, 0);
+ });
+}
+
+</script>
+</head>
+
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/931388.html b/dom/media/mediasource/test/crashtests/931388.html
new file mode 100644
index 000000000..cdb5bd9ad
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/931388.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<script>
+
+function boom()
+{
+ var v = document.createElement('video');
+ v.src = URL.createObjectURL(new MediaSource());
+ v.play();
+}
+
+</script>
+</head>
+<body onload="boom();"></body>
+</html>
diff --git a/dom/media/mediasource/test/crashtests/crashtests.list b/dom/media/mediasource/test/crashtests/crashtests.list
new file mode 100644
index 000000000..e16ec261d
--- /dev/null
+++ b/dom/media/mediasource/test/crashtests/crashtests.list
@@ -0,0 +1,4 @@
+test-pref(media.mediasource.enabled,true) load 926665.html
+test-pref(media.mediasource.enabled,true) load 931388.html
+test-pref(media.mediasource.enabled,true) load 1005366.html
+test-pref(media.mediasource.enabled,true) load 1059035.html
diff --git a/dom/media/mediasource/test/mediasource.js b/dom/media/mediasource/test/mediasource.js
new file mode 100644
index 000000000..6a464096a
--- /dev/null
+++ b/dom/media/mediasource/test/mediasource.js
@@ -0,0 +1,130 @@
+// Helpers for Media Source Extensions tests
+
+function runWithMSE(testFunction) {
+ function bootstrapTest() {
+ var ms = new MediaSource();
+
+ var el = document.createElement("video");
+ el.src = URL.createObjectURL(ms);
+ el.preload = "auto";
+
+ document.body.appendChild(el);
+ SimpleTest.registerCleanupFunction(function () {
+ el.parentNode.removeChild(el);
+ });
+
+ testFunction(ms, el);
+ }
+
+ addLoadEvent(function () {
+ SpecialPowers.pushPrefEnv({"set": [
+ [ "media.mediasource.enabled", true ],
+ ]},
+ bootstrapTest);
+ });
+}
+
+function fetchWithXHR(uri, onLoadFunction) {
+ var p = new Promise(function(resolve, reject) {
+ var xhr = new XMLHttpRequest();
+ xhr.open("GET", uri, true);
+ xhr.responseType = "arraybuffer";
+ xhr.addEventListener("load", function () {
+ is(xhr.status, 200, "fetchWithXHR load uri='" + uri + "' status=" + xhr.status);
+ resolve(xhr.response);
+ });
+ xhr.send();
+ });
+
+ if (onLoadFunction) {
+ p.then(onLoadFunction);
+ }
+
+ return p;
+};
+
+function range(start, end) {
+ var rv = [];
+ for (var i = start; i < end; ++i) {
+ rv.push(i);
+ }
+ return rv;
+}
+
+function once(target, name, cb) {
+ var p = new Promise(function(resolve, reject) {
+ target.addEventListener(name, function onceEvent() {
+ target.removeEventListener(name, onceEvent);
+ resolve();
+ });
+ });
+ if (cb) {
+ p.then(cb);
+ }
+ return p;
+}
+
+function timeRangeToString(r) {
+ var str = "TimeRanges: ";
+ for (var i = 0; i < r.length; i++) {
+ str += "[" + r.start(i) + ", " + r.end(i) + ")";
+ }
+ return str;
+}
+
+function loadSegment(sb, typedArrayOrArrayBuffer) {
+ var typedArray = (typedArrayOrArrayBuffer instanceof ArrayBuffer) ? new Uint8Array(typedArrayOrArrayBuffer)
+ : typedArrayOrArrayBuffer;
+ info(`Loading buffer: [${typedArray.byteOffset}, ${typedArray.byteOffset + typedArray.byteLength})`);
+ var beforeBuffered = timeRangeToString(sb.buffered);
+ return new Promise(function(resolve, reject) {
+ once(sb, 'update').then(function() {
+ var afterBuffered = timeRangeToString(sb.buffered);
+ info(`SourceBuffer buffered ranges grew from ${beforeBuffered} to ${afterBuffered}`);
+ resolve();
+ });
+ sb.appendBuffer(typedArray);
+ });
+}
+
+function fetchAndLoad(sb, prefix, chunks, suffix) {
+
+ // Fetch the buffers in parallel.
+ var buffers = {};
+ var fetches = [];
+ for (var chunk of chunks) {
+ fetches.push(fetchWithXHR(prefix + chunk + suffix).then(((c, x) => buffers[c] = x).bind(null, chunk)));
+ }
+
+ // Load them in series, as required per spec.
+ return Promise.all(fetches).then(function() {
+ var rv = Promise.resolve();
+ for (var chunk of chunks) {
+ rv = rv.then(loadSegment.bind(null, sb, buffers[chunk]));
+ }
+ return rv;
+ });
+}
+
+//Register timeout function to dump debugging logs.
+SimpleTest.registerTimeoutFunction(function() {
+ for (var v of document.getElementsByTagName("video")) {
+ v.mozDumpDebugInfo();
+ }
+ for (var a of document.getElementsByTagName("audio")) {
+ a.mozDumpDebugInfo();
+ }
+});
+
+function waitUntilTime(target, targetTime) {
+ return new Promise(function(resolve, reject) {
+ target.addEventListener("waiting", function onwaiting() {
+ info("Got a waiting event at " + target.currentTime);
+ if (target.currentTime >= targetTime) {
+ ok(true, "Reached target time of: " + targetTime);
+ target.removeEventListener("waiting", onwaiting);
+ resolve();
+ }
+ });
+ });
+}
diff --git a/dom/media/mediasource/test/mochitest.ini b/dom/media/mediasource/test/mochitest.ini
new file mode 100644
index 000000000..89ed35382
--- /dev/null
+++ b/dom/media/mediasource/test/mochitest.ini
@@ -0,0 +1,137 @@
+[DEFAULT]
+subsuite = media
+support-files =
+ mediasource.js
+ seek.webm seek.webm^headers^
+ seek_lowres.webm seek_lowres.webm^headers^
+ bipbop/bipbop2s.mp4 bipbop/bipbop2s.mp4^headers^
+ bipbop/bipbopinit.mp4 bipbop/bipbop_audioinit.mp4 bipbop/bipbop_videoinit.mp4
+ bipbop/bipbop1.m4s bipbop/bipbop_audio1.m4s bipbop/bipbop_video1.m4s
+ bipbop/bipbop2.m4s bipbop/bipbop_audio2.m4s bipbop/bipbop_video2.m4s
+ bipbop/bipbop3.m4s bipbop/bipbop_audio3.m4s bipbop/bipbop_video3.m4s
+ bipbop/bipbop4.m4s bipbop/bipbop_audio4.m4s bipbop/bipbop_video4.m4s
+ bipbop/bipbop5.m4s bipbop/bipbop_audio5.m4s bipbop/bipbop_video5.m4s
+ bipbop/bipbop6.m4s bipbop/bipbop_audio6.m4s bipbop/bipbop_video6.m4s
+ bipbop/bipbop7.m4s bipbop/bipbop_audio7.m4s bipbop/bipbop_video7.m4s
+ bipbop/bipbop8.m4s bipbop/bipbop_audio8.m4s bipbop/bipbop_video8.m4s
+ bipbop/bipbop9.m4s bipbop/bipbop_audio9.m4s bipbop/bipbop_video9.m4s
+ bipbop/bipbop10.m4s bipbop/bipbop_audio10.m4s bipbop/bipbop_video10.m4s
+ bipbop/bipbop11.m4s bipbop/bipbop_audio11.m4s bipbop/bipbop_video11.m4s
+ bipbop/bipbop12.m4s bipbop/bipbop_video12.m4s
+ bipbop/bipbop13.m4s bipbop/bipbop_video13.m4s
+ bipbop/bipbopinit.mp4^headers^ bipbop/bipbop_audioinit.mp4^headers^ bipbop/bipbop_videoinit.mp4^headers^
+ bipbop/bipbop1.m4s^headers^ bipbop/bipbop_audio1.m4s^headers^ bipbop/bipbop_video1.m4s^headers^
+ bipbop/bipbop2.m4s^headers^ bipbop/bipbop_audio2.m4s^headers^ bipbop/bipbop_video2.m4s^headers^
+ bipbop/bipbop3.m4s^headers^ bipbop/bipbop_audio3.m4s^headers^ bipbop/bipbop_video3.m4s^headers^
+ bipbop/bipbop4.m4s^headers^ bipbop/bipbop_audio4.m4s^headers^ bipbop/bipbop_video4.m4s^headers^
+ bipbop/bipbop5.m4s^headers^ bipbop/bipbop_audio5.m4s^headers^ bipbop/bipbop_video5.m4s^headers^
+ bipbop/bipbop6.m4s^headers^ bipbop/bipbop_audio6.m4s^headers^ bipbop/bipbop_video6.m4s^headers^
+ bipbop/bipbop7.m4s^headers^ bipbop/bipbop_audio7.m4s^headers^ bipbop/bipbop_video7.m4s^headers^
+ bipbop/bipbop8.m4s^headers^ bipbop/bipbop_audio8.m4s^headers^ bipbop/bipbop_video8.m4s^headers^
+ bipbop/bipbop9.m4s^headers^ bipbop/bipbop_audio9.m4s^headers^ bipbop/bipbop_video9.m4s^headers^
+ bipbop/bipbop10.m4s^headers^ bipbop/bipbop_audio10.m4s^headers^ bipbop/bipbop_video10.m4s^headers^
+ bipbop/bipbop11.m4s^headers^ bipbop/bipbop_audio11.m4s^headers^ bipbop/bipbop_video11.m4s^headers^
+ bipbop/bipbop12.m4s^headers^ bipbop/bipbop_video12.m4s^headers^
+ bipbop/bipbop13.m4s^headers^ bipbop/bipbop_video13.m4s^headers^
+ aac20-48000-64000-init.mp4 aac20-48000-64000-init.mp4^headers^
+ aac20-48000-64000-1.m4s aac20-48000-64000-1.m4s^headers^
+ aac20-48000-64000-2.m4s aac20-48000-64000-2.m4s^headers^
+ aac51-48000-128000-init.mp4 aac51-48000-128000-init.mp4^headers^
+ aac51-48000-128000-1.m4s aac51-48000-128000-1.m4s^headers^
+ aac51-48000-128000-2.m4s aac51-48000-128000-2.m4s^headers^
+ bipbop/bipbop_480_624kbps-videoinit.mp4 bipbop/bipbop_480_624kbps-videoinit.mp4^headers^
+ bipbop/bipbop_480_624kbps-video1.m4s bipbop/bipbop_480_624kbps-video1.m4s^headers^
+ bipbop/bipbop_480_624kbps-video2.m4s bipbop/bipbop_480_624kbps-video2.m4s^headers^
+
+[test_AudioChange_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_AutoRevocation.html]
+tags = firstpartyisolation
+[test_BufferedSeek.html]
+[test_BufferedSeek_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_BufferingWait.html]
+skip-if = toolkit == 'android' #timeout android bug 1199531
+[test_BufferingWait_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_DrainOnMissingData_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_DurationChange.html]
+[test_DurationUpdated.html]
+[test_DurationUpdated_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_EndOfStream.html]
+[test_EndOfStream_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_Eviction_mp4.html]
+skip-if = (os == "win" && os_version == "5.1") # Not supported on xp.
+[test_FrameSelection.html]
+[test_FrameSelection_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_HaveMetadataUnbufferedSeek.html]
+[test_HaveMetadataUnbufferedSeek_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_LiveSeekable.html]
+[test_LoadedDataFired_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_LoadedMetadataFired.html]
+[test_LoadedMetadataFired_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_MediaSource.html]
+[test_MediaSource_memory_reporting.html]
+[test_MediaSource_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_MediaSource_disabled.html]
+[test_MultipleInitSegments.html]
+[test_MultipleInitSegments_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_OnEvents.html]
+[test_PlayEvents.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_ResumeAfterClearing_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekableAfterEndOfStream.html]
+[test_SeekableAfterEndOfStream_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekableAfterEndOfStreamSplit.html]
+[test_SeekableAfterEndOfStreamSplit_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekableBeforeEndOfStream.html]
+[test_SeekableBeforeEndOfStream_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekableBeforeEndOfStreamSplit.html]
+[test_SeekableBeforeEndOfStreamSplit_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekNoData_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekedEvent_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekToEnd_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SeekTwice_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_Sequence_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SetModeThrows.html]
+[test_SplitAppendDelay.html]
+[test_SplitAppendDelay_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_SplitAppend.html]
+[test_SplitAppend_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_Threshold_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_TimestampOffset_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_TruncatedDuration.html]
+[test_TruncatedDuration_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_WaitingOnMissingData.html]
+skip-if = (toolkit == 'android') #timeout android only bug 1101187
+[test_WaitingOnMissingData_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_WaitingOnMissingDataEnded_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+[test_WaitingToEndedTransition_mp4.html]
+skip-if = ((os == "win" && os_version == "5.1") || (toolkit == 'android')) # Not supported on xp and android 2.3
+
diff --git a/dom/media/mediasource/test/seek.webm b/dom/media/mediasource/test/seek.webm
new file mode 100644
index 000000000..72b029723
--- /dev/null
+++ b/dom/media/mediasource/test/seek.webm
Binary files differ
diff --git a/dom/media/mediasource/test/seek.webm^headers^ b/dom/media/mediasource/test/seek.webm^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/seek.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/seek_lowres.webm b/dom/media/mediasource/test/seek_lowres.webm
new file mode 100644
index 000000000..8a76e0647
--- /dev/null
+++ b/dom/media/mediasource/test/seek_lowres.webm
Binary files differ
diff --git a/dom/media/mediasource/test/seek_lowres.webm^headers^ b/dom/media/mediasource/test/seek_lowres.webm^headers^
new file mode 100644
index 000000000..4030ea1d3
--- /dev/null
+++ b/dom/media/mediasource/test/seek_lowres.webm^headers^
@@ -0,0 +1 @@
+Cache-Control: no-store
diff --git a/dom/media/mediasource/test/test_AudioChange_mp4.html b/dom/media/mediasource/test/test_AudioChange_mp4.html
new file mode 100644
index 000000000..95f48cadf
--- /dev/null
+++ b/dom/media/mediasource/test/test_AudioChange_mp4.html
@@ -0,0 +1,72 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks loading a stereo segment, followed by a 5.1 segment plays without error.
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause", "durationchange", "seeking", "seeked"];
+ function logEvent(e) {
+ var v = e.target;
+ info("got " + e.type + " event");
+ }
+ events.forEach(function(e) {
+ el.addEventListener(e, logEvent, false);
+ });
+
+ ok(true, "Receive a sourceopen event");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+ el.addEventListener("error", function(e) {
+ ok(false, "should not fire '" + e.type + "' event");
+ SimpleTest.finish();
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ fetchAndLoad(audiosb, 'aac20-48000-64000-', ['init'], '.mp4')
+ .then(once.bind(null, el, 'loadedmetadata'))
+ .then(function() {
+ ok(true, "got loadedmetadata event");
+ var promises = [];
+ promises.push(once(el, 'loadeddata'));
+ promises.push(once(el, 'canplay'));
+ promises.push(fetchAndLoad(audiosb, 'aac20-48000-64000-', ['1'], '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got canplay event");
+ el.play();
+ return fetchAndLoad(audiosb, 'aac51-48000-128000-', ['init'], '.mp4');
+ })
+ .then(fetchAndLoad.bind(null, audiosb, 'aac51-48000-128000-', ['2'], '.m4s'))
+ .then(function() {
+ var promises = [];
+ ms.endOfStream();
+ promises.push(once(el, 'ended'));
+ promises.push(once(audiosb, 'updateend'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(el.currentTime >= 6, "played to the end");
+ SimpleTest.finish();
+ })
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_AutoRevocation.html b/dom/media/mediasource/test/test_AutoRevocation.html
new file mode 100644
index 000000000..15474367e
--- /dev/null
+++ b/dom/media/mediasource/test/test_AutoRevocation.html
@@ -0,0 +1,40 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: auto-revocation</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function () {
+ var ms = new MediaSource();
+ var o = URL.createObjectURL(ms);
+ var v = document.createElement("video");
+
+ v.addEventListener("error", function (e) {
+ ok(true, "ObjectURL should be auto-revoked");
+ SimpleTest.finish();
+ });
+
+ v.addEventListener("stalled", function (e) {
+ ok(false, "If auto-revocation is gone, please turn on TODOs in browser_mediaSourceURL.js");
+ SimpleTest.finish();
+ });
+
+ setTimeout(function() {
+ v.src = o;
+ v.preload = "auto";
+ document.body.appendChild(v);
+ }, 0);
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferedSeek.html b/dom/media/mediasource/test/test_BufferedSeek.html
new file mode 100644
index 000000000..949fee773
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferedSeek.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seeking in buffered range</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ };
+ });
+ });
+
+ var target = 2;
+
+ v.addEventListener("loadedmetadata", function () {
+ if (v.currentTime != target &&
+ v.buffered.length &&
+ target >= v.buffered.start(0) &&
+ target < v.buffered.end(0)) {
+ v.currentTime = target;
+ }
+ });
+
+ var wasSeeking = false;
+
+ v.addEventListener("seeking", function () {
+ wasSeeking = true;
+ is(v.currentTime, target, "Video currentTime at target");
+ });
+
+ v.addEventListener("seeked", function () {
+ ok(wasSeeking, "Received expected seeking and seeked events");
+ is(v.currentTime, target, "Video currentTime at target");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferedSeek_mp4.html b/dom/media/mediasource/test/test_BufferedSeek_mp4.html
new file mode 100644
index 000000000..7c5d992f3
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferedSeek_mp4.html
@@ -0,0 +1,63 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seeking in buffered range</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ };
+ });
+ });
+
+ var target = 1.3;
+
+ v.addEventListener("loadedmetadata", function () {
+ if (v.currentTime != target &&
+ v.buffered.length &&
+ target >= v.buffered.start(0) &&
+ target < v.buffered.end(0)) {
+ v.currentTime = target;
+ }
+ });
+
+ var wasSeeking = false;
+
+ v.addEventListener("seeking", function () {
+ wasSeeking = true;
+ is(v.currentTime, target, "Video currentTime at target");
+ });
+
+ v.addEventListener("seeked", function () {
+ ok(wasSeeking, "Received expected seeking and seeked events");
+ is(v.currentTime, target, "Video currentTime at target");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferingWait.html b/dom/media/mediasource/test/test_BufferingWait.html
new file mode 100644
index 000000000..8d45132b4
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferingWait.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var receivedSourceOpen = false;
+runWithMSE(function(ms, v) {
+ ms.addEventListener("sourceopen", function() {
+ ok(true, "Receive a sourceopen event");
+ ok(!receivedSourceOpen, "Should only receive one sourceopen for this test");
+ receivedSourceOpen = true;
+ var sb = ms.addSourceBuffer("video/webm");
+ ok(sb, "Create a SourceBuffer");
+
+ fetchWithXHR("seek.webm", function(arrayBuffer) {
+ sb.addEventListener('error', (e) => { ok(false, "Got Error: " + e); SimpleTest.finish(); });
+ loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 0, 318))().then(
+ loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 318, 25523-318))).then(
+ loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 25523, 46712-25523))).then(
+ /* Note - Missing |46712, 67833 - 46712| segment here corresponding to (0.8, 1.2] */
+ /* Note - Missing |67833, 88966 - 67833| segment here corresponding to (1.2, 1.6] */
+ loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 88966))).then(function() {
+ // 0.767 is the time of the last video sample +- 40ms.
+ var promise = waitUntilTime(v, .767-0.04);
+ info("Playing video. It should play for a bit, then fire 'waiting'");
+ v.play();
+ return promise;
+ }).then(function() {
+ window.firstStop = Date.now();
+ loadSegment(sb, new Uint8Array(arrayBuffer, 46712, 67833 - 46712));
+ return waitUntilTime(v, 1.167-0.04);
+ }).then(function() {
+ var waitDuration = (Date.now() - window.firstStop) / 1000;
+ ok(waitDuration < 15, "Should not spend an inordinate amount of time buffering: " + waitDuration);
+ SimpleTest.finish();
+ /* If we allow the rest of the stream to be played, we get stuck at
+ around 2s. See bug 1093133.
+ once(v, 'ended', SimpleTest.finish.bind(SimpleTest));
+ return loadSegment(sb, new Uint8Array(arrayBuffer, 67833, 88966 - 67833));
+ */
+ });
+ });
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_BufferingWait_mp4.html b/dom/media/mediasource/test/test_BufferingWait_mp4.html
new file mode 100644
index 000000000..cb262b525
--- /dev/null
+++ b/dom/media/mediasource/test/test_BufferingWait_mp4.html
@@ -0,0 +1,54 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var receivedSourceOpen = false;
+runWithMSE(function(ms, v) {
+ ms.addEventListener("sourceopen", function() {
+ ok(true, "Receive a sourceopen event");
+ ok(!receivedSourceOpen, "Should only receive one sourceopen for this test");
+ receivedSourceOpen = true;
+ var sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+
+ sb.addEventListener('error', (e) => { ok(false, "Got Error: " + e); SimpleTest.finish(); });
+ fetchAndLoad(sb, 'bipbop/bipbop', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, sb, 'bipbop/bipbop', ['1'], '.m4s'))
+ .then(fetchAndLoad.bind(null, sb, 'bipbop/bipbop', ['2'], '.m4s'))
+ /* Note - Missing |bipbop3| segment here corresponding to (1.62, 2.41] */
+ /* Note - Missing |bipbop4| segment here corresponding to (2.41, 3.20] */
+ .then(fetchAndLoad.bind(null, sb, 'bipbop/bipbop', ['5'], '.m4s'))
+ .then(function() {
+ // last audio sample has a start time of 1.578956s
+ var promise = waitUntilTime(v, 1.57895);
+ info("Playing video. It should play for a bit, then fire 'waiting'");
+ v.play();
+ return promise;
+ }).then(function() {
+ window.firstStop = Date.now();
+ fetchAndLoad(sb, 'bipbop/bipbop', ['3'], '.m4s');
+ // last audio sample has a start time of 2.368435
+ return waitUntilTime(v, 2.36843);
+ }).then(function() {
+ var waitDuration = (Date.now() - window.firstStop) / 1000;
+ ok(waitDuration < 15, "Should not spend an inordinate amount of time buffering: " + waitDuration);
+ once(v, 'ended', SimpleTest.finish.bind(SimpleTest));
+ return fetchAndLoad(sb, 'bipbop/bipbop', ['4'], '.m4s');
+ }).then(function() {
+ ms.endOfStream();
+ });;
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DrainOnMissingData_mp4.html b/dom/media/mediasource/test/test_DrainOnMissingData_mp4.html
new file mode 100644
index 000000000..f2284377f
--- /dev/null
+++ b/dom/media/mediasource/test/test_DrainOnMissingData_mp4.html
@@ -0,0 +1,60 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(function() {
+ // Set appendWindowEnd to ensure we only have about 6 frames worth.
+ // We must feed at least 6 frames to pass the MDSM pre-roll.
+ videosb.appendWindowEnd = .4;
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', ['1'], '.m4s');
+ })
+ .then(function() {
+ info("Invoking play()");
+ var promises = [];
+ promises.push(once(el, 'playing'));
+ el.play();
+ return Promise.all(promises);
+ })
+ .then(function() {
+ info("got playing");
+ return once(el, 'waiting');
+ }).then(function() {
+ info("got waiting");
+ info("Loading more data");
+ // Waiting will be fired on the last frame +- 40ms.
+ isfuzzy(el.currentTime, videosb.buffered.end(0) - 1/30,
+ 0.04, "Got a waiting event at " + el.currentTime);
+ videosb.appendWindowEnd = 1;
+ var p = once(el, 'ended');
+ var loads = fetchAndLoad(videosb, 'bipbop/bipbop_video', [1], '.m4s');
+ loads.then(() => ms.endOfStream());
+ return p;
+ }).then(function() {
+ // These fuzz factors are bigger than they should be. We should investigate
+ // and fix them in bug 1137574.
+ is(el.duration, 0.801666, "Video has correct duration: " + el.duration);
+ is(el.currentTime, el.duration, "Video has correct currentTime.");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DurationChange.html b/dom/media/mediasource/test/test_DurationChange.html
new file mode 100644
index 000000000..58c58be15
--- /dev/null
+++ b/dom/media/mediasource/test/test_DurationChange.html
@@ -0,0 +1,111 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: check that duration change behaves properly</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ once(v, "loadedmetadata")
+ .then(function() {
+ is(v.duration, ms.duration, "video duration is mediasource one");
+ try {
+ ms.duration = 0;
+ } catch (e) { ok(false, "must not throw as operation is valid"); }
+ is(v.duration, 0, "reducing duration with no data buffered is valid");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ // Adding more data will fire durationchange.
+ once(sb, "updateend")
+ .then(function() {
+ ok(true, "got updateend");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ var error = false;
+ try {
+ ms.duration = 0;
+ } catch (e) {
+ ok(true, "must use remove for range removal");
+ is(e.name, "InvalidStateError", "Error is InvalidStateError");
+ error = true;
+ }
+ ok(error, "got an error");
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ try {
+ ms.duration = 10;
+ } catch (e) { ok(false, "must not throw as setting duration past data is valid"); }
+ is(v.duration, 10, "extending duration is always valid");
+ // The last sample has a start time of 3.967000s and a end time of 4.001 (see bug 1065207).
+ try {
+ ms.duration = 3.967000;
+ } catch (e) { ok(false, "setting duration with >= highest frame presentation time is valid"); }
+ is(v.duration, sb.buffered.end(0), "duration is the highest end time reported by the buffered attribute ");
+ try {
+ ms.duration = 3.97;
+ } catch (e) { ok(false, "setting duration with >= highest frame presentation time is valid"); }
+ is(v.duration, sb.buffered.end(0), "duration is the highest end time reported by the buffered attribute ");
+ error = false;
+ try {
+ ms.duration = 3.96;
+ } catch (e) {
+ ok(true, "setting duration with < highest frame presentation time is not valid");
+ is(e.name, "InvalidStateError", "Error is InvalidStateError");
+ error = true;
+ }
+ ok(error, "got an error");
+ is(v.duration, sb.buffered.end(0), "duration is the highest end time reported by the buffered attribute ");
+ error = false;
+ try {
+ ms.duration = -1;
+ } catch (e) {
+ ok(true, "can't set a negative duration");
+ is(e.name, "TypeError", "Error is TypeError");
+ error = true;
+ }
+ ok(error, "got an error");
+ sb.remove(sb.buffered.end(0), Infinity);
+ is(sb.updating, true, "updating is true")
+ error = false;
+ try {
+ ms.duration = Infinity;
+ } catch (e) {
+ ok(true, "setting the duration while updating is not allowed");
+ is(e.name, "InvalidStateError", "Error is InvalidStateError");
+ error = true;
+ }
+ ok(error, "got an error");
+ error = false;
+ try {
+ sb.abort();
+ } catch (e) {
+ ok(true, "Can't use abort while range removal is in progress");
+ is(e.name, "InvalidStateError", "Error is InvalidStateError");
+ error = true;
+ }
+ ok(error, "got an error");
+ is(v.duration, sb.buffered.end(0), "duration is the highest end time reported by the buffered attribute ");
+ once(sb, "updateend", () => ms.endOfStream());
+ });
+ });
+ });
+ });
+ ms.addEventListener("sourceended", function () {
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DurationUpdated.html b/dom/media/mediasource/test/test_DurationUpdated.html
new file mode 100644
index 000000000..0ee6a5459
--- /dev/null
+++ b/dom/media/mediasource/test/test_DurationUpdated.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append data and check that mediasource duration got updated</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var durationChangeCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ v.addEventListener("durationchange", function () {
+ durationChangeCount++;
+ });
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ // Adding the first init segment will fire a durationchange.
+ once(v, "loadedmetadata")
+ .then(function() {
+ ok(true, "got loadedmetadata");
+ // Set mediasource duration to 0, so future appendBuffer
+ // will update the mediasource duration.
+ // Changing the duration will fire a durationchange.
+ ms.duration = 0;
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ // Adding more data will fire durationchange.
+ once(sb, "updateend")
+ .then(function() {
+ ok(true, "got updateend");
+ // this will not fire durationchange as new duration == old duration
+ ms.endOfStream();
+ });
+ });
+ });
+ });
+ ms.addEventListener("sourceended", function () {
+ is(durationChangeCount, 3, "durationchange not fired as many times as expected");
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_DurationUpdated_mp4.html b/dom/media/mediasource/test/test_DurationUpdated_mp4.html
new file mode 100644
index 000000000..da14267eb
--- /dev/null
+++ b/dom/media/mediasource/test/test_DurationUpdated_mp4.html
@@ -0,0 +1,56 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append data and check that mediasource duration got updated</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var durationChangeCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ v.addEventListener("durationchange", function () {
+ durationChangeCount++;
+ });
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+ // Adding the first init segment will fire a durationchange.
+ once(v, "loadedmetadata")
+ .then(function() {
+ ok(true, "got loadedmetadata");
+ // Set mediasource duration to 0, so future appendBuffer
+ // will update the mediasource duration.
+ // Changing the duration will fire a durationchange.
+ ms.duration = 0;
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 1395));
+ // Adding more data will fire durationchange.
+ once(sb, "updateend")
+ .then(function() {
+ ok(true, "got updateend");
+ // this will not fire durationchange as new duration == old duration
+ ms.endOfStream();
+ });
+ });
+ });
+ });
+ ms.addEventListener("sourceended", function () {
+ is(durationChangeCount, 3, "durationchange not fired as many times as expected");
+ is(v.duration, 1.696666, "Video has correct duration");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_EndOfStream.html b/dom/media/mediasource/test/test_EndOfStream.html
new file mode 100644
index 000000000..655d21cd3
--- /dev/null
+++ b/dom/media/mediasource/test/test_EndOfStream.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: endOfStream call after an appendBuffer</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function () {
+ var ms = new MediaSource();
+
+ var v = document.createElement("video");
+ v.src = URL.createObjectURL(ms);
+ document.body.appendChild(v);
+
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 88966));
+ var count = 0;
+ sb.addEventListener("updateend", function () {
+ ++count;
+ if (count == 1) {
+ setTimeout(function() {
+ var fail = false;
+ try {
+ ms.endOfStream();
+ } catch (e) {
+ fail = true;
+ }
+ ok(!fail, "MediaSource.endOfStream succeeded");
+ SimpleTest.finish();
+ }, 0);
+ }
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_EndOfStream_mp4.html b/dom/media/mediasource/test/test_EndOfStream_mp4.html
new file mode 100644
index 000000000..1f703f2b6
--- /dev/null
+++ b/dom/media/mediasource/test/test_EndOfStream_mp4.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: endOfStream call after an appendBuffer</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function () {
+ var ms = new MediaSource();
+
+ var v = document.createElement("video");
+ v.src = URL.createObjectURL(ms);
+ document.body.appendChild(v);
+
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ var count = 0;
+ sb.addEventListener("updateend", function () {
+ ++count;
+ if (count == 1) {
+ setTimeout(function() {
+ var fail = false;
+ try {
+ ms.endOfStream();
+ } catch (e) {
+ fail = true;
+ }
+ ok(!fail, "MediaSource.endOfStream succeeded");
+ SimpleTest.finish();
+ }, 0);
+ }
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Eviction_mp4.html b/dom/media/mediasource/test/test_Eviction_mp4.html
new file mode 100644
index 000000000..c702cc3bb
--- /dev/null
+++ b/dom/media/mediasource/test/test_Eviction_mp4.html
@@ -0,0 +1,82 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: QuotaExceededError when source buffer is full</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+// We fill up the source buffer with audio data until the buffer is full.
+// We ensure that QuotaExceededError is thrown once the buffer is full.
+// We then seek to half the content. By that time, another appendBuffer must succeed
+// as the auto-eviction would succeed (removing all data prior currentTime)
+
+// Fill up the SourceBuffer by appending data repeatedly via doAppendDataFunc until
+// an exception is thrown.
+function fillUpSourceBuffer(sourceBuffer, doAppendDataFunc, onCaughtExceptionCallback) {
+ // We are appending data repeatedly in sequence mode, there should be no gaps.
+ ok(sourceBuffer.buffered.length <= 1, "there should be no gap in buffered ranges.");
+ try {
+ doAppendDataFunc();
+ } catch(ex) {
+ onCaughtExceptionCallback(ex);
+ return;
+ }
+ once(sourceBuffer, 'updateend', () => {
+ fillUpSourceBuffer(sourceBuffer, doAppendDataFunc, onCaughtExceptionCallback);
+ });
+}
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["media.mediasource.eviction_threshold.audio", 524288],
+ ]
+ }, function() {
+ let audiosb = ms.addSourceBuffer("audio/mp4");
+ audiosb.mode = "sequence";
+ fetchAndLoad(audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4')
+ .then(function() {
+ fetchWithXHR('bipbop/bipbop_audio1.m4s', function(audioBuffer) {
+ fillUpSourceBuffer(audiosb,
+ function() { // doAppendDataFunc
+ audiosb.appendBuffer(audioBuffer);
+ },
+ function(ex) { // onCaughtExceptionCallback
+ is(ex.name, 'QuotaExceededError', "QuotaExceededError thrown");
+ is(audiosb.buffered.end(0), el.duration, "Duration is end of buffered range");
+ let seekTime = audiosb.buffered.end(0) / 2;
+ el.currentTime = seekTime;
+ once(el, 'seeked', () => {
+ is(el.currentTime, seekTime, "correctly seeked to " + seekTime);
+ try {
+ audiosb.appendBuffer(audioBuffer);
+ } catch(ex) {
+ ok(false, "Shouldn't throw another time when data can be evicted");
+ el.mozDumpDebugInfo();
+ SimpleTest.finish();
+ return;
+ }
+ once(audiosb, 'update', () => {
+ ok(true, "appendBuffer succeeded");
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_FrameSelection.html b/dom/media/mediasource/test/test_FrameSelection.html
new file mode 100644
index 000000000..f9190af9c
--- /dev/null
+++ b/dom/media/mediasource/test/test_FrameSelection.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: verify correct frames selected for given position</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+ var targets = [{ currentTime: 3, videoWidth: 160, videoHeight: 120 },
+ { currentTime: 2, videoWidth: 160, videoHeight: 120 },
+ { currentTime: 0, videoWidth: 320, videoHeight: 240 }];
+ var target;
+
+var lowResBuffer;
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm")
+ .then(function (arrayBuffer) {
+ var p = once(v, 'loadedmetadata');
+ // Append entire file covering range [0, 4].
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ return p;
+ }).then(function() {
+ is(v.currentTime, 0, "currentTime has correct initial value");
+ is(v.videoWidth, 320, "videoWidth has correct initial value");
+ is(v.videoHeight, 240, "videoHeight has correct initial value");
+ return fetchWithXHR("seek_lowres.webm");
+ }).then(function (arrayBuffer) {
+ // Append initialization segment.
+ var p = once(sb, 'updateend');
+ info("Appending low-res init segment");
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 438));
+ lowResBuffer = arrayBuffer;
+ return p;
+ }).then(function() {
+ var p = once(sb, 'updateend');
+ info("Appending low-res range [2,4]");
+ // Append media segment covering range [2, 4].
+ sb.appendBuffer(new Uint8Array(lowResBuffer, 51003));
+ return p;
+ }).then(function() {
+ ms.endOfStream();
+ var p = Promise.all([once(v, 'seeked'), once(v, 'resize')]);
+ info("Seeking to t=3");
+ v.currentTime = 3;
+ return p;
+ }).then(function() {
+ is(v.currentTime, 3, "Video currentTime at target");
+ is(v.videoWidth, 160, "videoWidth has correct low-res value");
+ is(v.videoHeight, 120, "videoHeight has correct low-res value");
+
+ var p = Promise.all([once(v, 'seeked'), once(v, 'resize')]);
+ info("Seeking to t=1");
+ v.currentTime = 1;
+ return p;
+ }).then(function() {
+ is(v.currentTime, 1, "Video currentTime at target");
+ is(v.videoWidth, 320, "videoWidth has correct high-res value");
+ is(v.videoHeight, 240, "videoHeight has correct high-res value");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_FrameSelection_mp4.html b/dom/media/mediasource/test/test_FrameSelection_mp4.html
new file mode 100644
index 000000000..a5b8fa290
--- /dev/null
+++ b/dom/media/mediasource/test/test_FrameSelection_mp4.html
@@ -0,0 +1,73 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test loads partial video, plays and waits until playback stalls.
+// It then loads only 3 frames of a video at higher resolution.
+
+var receivedSourceOpen = false;
+runWithMSE(function(ms, v) {
+ ms.addEventListener("sourceopen", function() {
+ ok(true, "Receive a sourceopen event");
+ ok(!receivedSourceOpen, "Should only receive one sourceopen for this test");
+ receivedSourceOpen = true;
+ var sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause", "durationchange", "seeking", "seeked"];
+ function logEvent(e) {
+ var v = e.target;
+ info("got " + e.type + " event");
+ }
+ events.forEach(function(e) {
+ v.addEventListener(e, logEvent, false);
+ });
+
+ sb.addEventListener('error', (e) => { ok(false, "Got Error: " + e); SimpleTest.finish(); });
+ fetchAndLoad(sb, 'bipbop/bipbop', ['init'], '.mp4')
+ .then(function() {
+ var promises = [];
+ promises.push(fetchAndLoad(sb, 'bipbop/bipbop', range(1,3), '.m4s'));
+ promises.push(once(v, "loadeddata"));
+ return Promise.all(promises);
+ }).then(function() {
+ is(sb.buffered.length, 1, "continuous range");
+ v.play();
+ // We have nothing to play, waiting will be fired.
+ return waitUntilTime(v, 1.5);
+ }).then(function() {
+ return fetchAndLoad(sb, 'bipbop/bipbop_480_624kbps-video', ['init'], '.mp4');
+ }).then(function() {
+ sb.timestampOffset = 1.601666; // End of the video track buffered - time of first video sample (0.095).
+ sb.appendWindowEnd = 1.796677; // Only allow room for three extra video frames (we need 3 as this video has b-frames).
+ return fetchAndLoad(sb, 'bipbop/bipbop_480_624kbps-video', ['1'], '.m4s');
+ }).then(function() {
+ ms.endOfStream();
+ var promises = [];
+ promises.push(once(ms, "sourceended"));
+ promises.push(once(v, "playing"));
+ promises.push(once(v, "ended"));
+ return Promise.all(promises);
+ }).then(function() {
+ if(v.width, 640, "has proper width");
+ if(v.height, 480, "has proper height");
+ SimpleTest.finish();
+ });
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html
new file mode 100644
index 000000000..f6192074a
--- /dev/null
+++ b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 67833));
+ });
+
+ var target = 2;
+
+ v.addEventListener("loadeddata", function onloadeddata() {
+ v.removeEventListener("loadeddata", onloadeddata);
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "readyState is >= CURRENT_DATA");
+ v.currentTime = target;
+ });
+
+ v.addEventListener("seeking", function () {
+ is(v.readyState, v.HAVE_METADATA, "readyState is HAVE_METADATA");
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 67833));
+ });
+ });
+
+ v.addEventListener("seeked", function () {
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html
new file mode 100644
index 000000000..fc3ef06eb
--- /dev/null
+++ b/dom/media/mediasource/test/test_HaveMetadataUnbufferedSeek_mp4.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25819));
+ });
+
+ var target = 1.3;
+
+ v.addEventListener("loadeddata", function onloadeddata() {
+ v.removeEventListener("loadeddata", onloadeddata);
+ ok(v.readyState >= v.HAVE_CURRENT_DATA, "readyState is >= CURRENT_DATA");
+ v.currentTime = target;
+ });
+
+ v.addEventListener("seeking", function () {
+ is(v.readyState, v.HAVE_METADATA, "readyState is HAVE_METADATA");
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ // 25819 is the offset of the first media segment's end
+ sb.addEventListener("updateend", function () {
+ ms.endOfStream();
+ });
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25819));
+ });
+ });
+
+ v.addEventListener("seeked", function () {
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LiveSeekable.html b/dom/media/mediasource/test/test_LiveSeekable.html
new file mode 100644
index 000000000..6abc42450
--- /dev/null
+++ b/dom/media/mediasource/test/test_LiveSeekable.html
@@ -0,0 +1,80 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: live seekable range</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+ // Load data with a +2 offset so that we can distinguish buffered range start
+ // and seekable range start.
+ sb.timestampOffset = 2;
+ var promises = [];
+ promises.push(fetchAndLoad(sb, 'seek', [''], '.webm'));
+ promises.push(once(v, "loadedmetadata"));
+ Promise.all(promises)
+ .then(function() {
+ ms.duration = Infinity;
+ sb.abort();
+ is(sb.buffered.length, 1, "continuous buffered range");
+ is(sb.buffered.start(0), 2, "buffered range start at timestamp offset");
+ is(sb.buffered.end(0), 6.001, "buffered range end at original duration + timestamp offset");
+ is(v.seekable.length, 1, "continuous seekable range");
+ is(v.seekable.start(0), 0, "seekable range start at 0");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ // LiveSeekableRange.start < buffered.start
+ ms.setLiveSeekableRange(1, 5);
+ is(v.seekable.length, 1, "continuous seekable range");
+ is(v.seekable.start(0), 1, "seekable range start at live range start");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ ms.clearLiveSeekableRange();
+ ok(v.seekable.length, 1, "continuous seekable range");
+ is(v.seekable.start(0), 0, "seekable range start at 0");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ // LiveSeekableRange.end > buffered.end
+ ms.setLiveSeekableRange(1, 8);
+ is(v.seekable.start(0), 1, "seekable range start at live range start");
+ is(v.seekable.end(0), 8, "seekable range end at live range end");
+
+ // LiveSeekableRange.start > buffered.start
+ // LiveSeekableRange.end < buffered.end
+ ms.setLiveSeekableRange(3, 5);
+ is(v.seekable.start(0), sb.buffered.start(0), "seekable range start at buffered start");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at live range end");
+
+ // LiveSeekableRange.start > buffered.end
+ ms.setLiveSeekableRange(8, 10);
+ is(v.seekable.start(0), sb.buffered.start(0), "seekable range start at buffered start");
+ is(v.seekable.end(0), 10, "seekable range end at live range end");
+
+ // LiveSeekableRange.end < buffered.start
+ ms.setLiveSeekableRange(0, 2);
+ is(v.seekable.start(0), 0, "seekable range start at live range start");
+ is(v.seekable.end(0), sb.buffered.end(0), "seekable range end at buffered end");
+
+ try {
+ ms.setLiveSeekableRange(2, 0);
+ ok(false, "start > end");
+ } catch (e) { ok(true, "must thow if start > end"); }
+
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LoadedDataFired_mp4.html b/dom/media/mediasource/test/test_LoadedDataFired_mp4.html
new file mode 100644
index 000000000..9b15c8063
--- /dev/null
+++ b/dom/media/mediasource/test/test_LoadedDataFired_mp4.html
@@ -0,0 +1,68 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: Check that playback only starts once we have data at time = 0</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ el.addEventListener("loadeddata", function() {
+ ok(el.buffered.length > 0, "data is buffered");
+ is(el.buffered.start(0), 0, "must fire loadeddata when data has been loaded");
+ is(el.currentTime, 0, "must fire loadeddata at start");
+ });
+ el.addEventListener("playing", function() {
+ ok(el.buffered.length > 0, "data is buffered");
+ is(el.buffered.start(0), 0, "must fire playing when data has been loaded");
+ ok(el.currentTime >= 0, "must have started playback");
+ });
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(once.bind(null, el, "loadedmetadata"))
+ .then(function() {
+ videosb.appendWindowStart = 2;
+ videosb.appendWindowEnd = 4;
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // Load [2.4, 3.968344). 2.4 as it's the first keyframe after 2s and
+ // 3.968344 as the last frame ends after 4s.
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(1, 8), '.m4s');
+ })
+ .then(function() {
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // test that appendWindowEnd did its job.
+ ok(el.buffered.start(0) >= 2, "no data can be found prior appendWindowStart");
+ ok(el.buffered.end(el.buffered.length-1) <= 4, "no data can be found beyond appendWindowEnd");
+ el.play();
+ return once(el, "play");
+ })
+ .then(function() {
+ videosb.appendWindowStart = 0;
+ var promises = [];
+ // Load [0, 3.971666).
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(1, 8), '.m4s'));
+ // playback can only start now.
+ promises.push(once(el, "playing"));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "playing");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LoadedMetadataFired.html b/dom/media/mediasource/test/test_LoadedMetadataFired.html
new file mode 100644
index 000000000..a26590537
--- /dev/null
+++ b/dom/media/mediasource/test/test_LoadedMetadataFired.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization only</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(true, "Got loadedmetadata event");
+ is(v.videoWidth, 320, "videoWidth has correct initial value");
+ is(v.videoHeight, 240, "videoHeight has correct initial value");
+ SimpleTest.finish();
+ });
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ v.play();
+ });
+ });
+
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html b/dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html
new file mode 100644
index 000000000..6d7547cf1
--- /dev/null
+++ b/dom/media/mediasource/test/test_LoadedMetadataFired_mp4.html
@@ -0,0 +1,37 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization only</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(true, "Got loadedmetadata event");
+ is(v.videoWidth, 400, "videoWidth has correct initial value");
+ is(v.videoHeight, 300, "videoHeight has correct initial value");
+ SimpleTest.finish();
+ });
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+ v.play();
+ });
+ });
+
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource.html b/dom/media/mediasource/test/test_MediaSource.html
new file mode 100644
index 000000000..6acd243e1
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource.html
@@ -0,0 +1,107 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function () {
+ SimpleTest.doesThrow(() => new SourceBuffer, "new SourceBuffer should fail");
+ SimpleTest.doesThrow(() => new SourceBufferList, "new SourceBufferList direct should fail");
+
+ var ms = new MediaSource();
+ ok(ms, "Create a MediaSource object");
+ ok(ms instanceof EventTarget, "MediaSource must be an EventTarget");
+ is(ms.readyState, "closed", "New MediaSource must be in closed state");
+
+ // Wrapper creation, tests for leaks.
+ SpecialPowers.wrap(ms);
+
+ // Set an expando to force wrapper creation, tests for leaks.
+ ms.foo = null;
+
+ var o = URL.createObjectURL(ms);
+ ok(o, "Create an objectURL from the MediaSource");
+
+ var v = document.createElement("video");
+ v.preload = "auto";
+ v.src = o;
+ document.body.appendChild(v);
+
+ var loadedmetadataCount = 0;
+ var updatestartCount = 0;
+ var updateendCount = 0;
+ var updateCount = 0;
+
+ ms.addEventListener("sourceopen", function () {
+ ok(true, "Receive a sourceopen event");
+ is(ms.readyState, "open", "MediaSource must be in open state after sourceopen");
+ var sb = ms.addSourceBuffer("video/webm");
+ ok(sb, "Create a SourceBuffer");
+ is(ms.sourceBuffers.length, 1, "MediaSource.sourceBuffers is expected length");
+ is(ms.sourceBuffers[0], sb, "SourceBuffer in list matches our SourceBuffer");
+ is(ms.activeSourceBuffers.length, 0, "MediaSource.activeSourceBuffers is expected length");
+
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ is(sb.updating, true, "SourceBuffer.updating is expected value after appendBuffer");
+ });
+
+ sb.addEventListener("update", function () {
+ is(sb.updating, false, "SourceBuffer.updating is expected value in update event");
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ }
+ });
+
+ sb.addEventListener("updatestart", function () {
+ updatestartCount++;
+ });
+
+ sb.addEventListener("updateend", function () {
+ is(ms.activeSourceBuffers[0], sb, "SourceBuffer in active list matches our SourceBuffer");
+ is(sb.updating, false, "SourceBuffer.updating is expected value in updateend event");
+ updateendCount++;
+ v.play();
+ });
+ });
+
+ ms.addEventListener("sourceended", function () {
+ ok(true, "Receive a sourceended event");
+ is(ms.readyState, "ended", "MediaSource must be in ended state after sourceended");
+ });
+
+ v.addEventListener("loadedmetadata", function () {
+ loadedmetadataCount++;
+ });
+
+ v.addEventListener("ended", function () {
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ ok(Math.abs(v.currentTime - 4) <= 0.002, "Video has played to end");
+ // XXX: 2 update events can be received dueto duration differences, see bug 1065207.
+ ok(updateCount == 1 || updateCount == 2, "update event received");
+ ok(updateendCount == 1 || updateendCount == 2, "updateend event received");
+ ok(updatestartCount == 1 || updatestartCount == 2, "updatestart event received");
+ is(loadedmetadataCount, 1, "loadedmetadata event received");
+ v.parentNode.removeChild(v);
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_disabled.html b/dom/media/mediasource/test/test_MediaSource_disabled.html
new file mode 100644
index 000000000..ccf36914a
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_disabled.html
@@ -0,0 +1,32 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: disabling via pref</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function test() {
+ ok(!window.MediaSource && !window.SourceBuffer && !window.SourceBufferList,
+ "MediaSource should be hidden behind a pref");
+ SimpleTest.doesThrow(() => new MediaSource,
+ "MediaSource should be hidden behind a pref");
+ SimpleTest.finish();
+}
+
+SpecialPowers.pushPrefEnv({"set":
+ [
+ ["media.mediasource.enabled", false],
+ ]
+ },
+ test);
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_memory_reporting.html b/dom/media/mediasource/test/test_MediaSource_memory_reporting.html
new file mode 100644
index 000000000..278b8760d
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_memory_reporting.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: memory reporting</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ // Test that memory reporting works once we've played a video.
+ once(v, "stalled", () => {
+ // Grab a memory report.
+ var mgr = SpecialPowers.Cc["@mozilla.org/memory-reporter-manager;1"]
+ .getService(SpecialPowers.Ci.nsIMemoryReporterManager);
+
+ var amount = 0;
+ var resourcesPathSeen = false;
+ var handleReport = function(aProcess, aPath, aKind, aUnits, aAmount, aDesc) {
+ if (aPath == "explicit/media/resources") {
+ resourcePathSeen = true;
+ amount += aAmount;
+ }
+ }
+
+ var finished = function () {
+ ok(true, "Yay didn't crash!");
+ ok(resourcePathSeen, "Got media resources amount");
+ ok(amount > 0, "Non-zero amount reported for media resources");
+ SimpleTest.finish();
+ }
+
+ mgr.getReports(handleReport, null, finished, null, /* anonymized = */ false);
+ });
+
+ // Load a webm video and play it.
+ ms.addEventListener("sourceopen", () => {
+ var sb = ms.addSourceBuffer("video/webm");
+ fetchAndLoad(sb, 'seek', [''], '.webm').then(() => v.play());
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MediaSource_mp4.html b/dom/media/mediasource/test/test_MediaSource_mp4.html
new file mode 100644
index 000000000..fc5ac2895
--- /dev/null
+++ b/dom/media/mediasource/test/test_MediaSource_mp4.html
@@ -0,0 +1,108 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function () {
+ SimpleTest.doesThrow(() => new SourceBuffer, "new SourceBuffer should fail");
+ SimpleTest.doesThrow(() => new SourceBufferList, "new SourceBufferList direct should fail");
+
+ var ms = new MediaSource();
+ ok(ms, "Create a MediaSource object");
+ ok(ms instanceof EventTarget, "MediaSource must be an EventTarget");
+ is(ms.readyState, "closed", "New MediaSource must be in closed state");
+
+ // Wrapper creation, tests for leaks.
+ SpecialPowers.wrap(ms);
+
+ // Set an expando to force wrapper creation, tests for leaks.
+ ms.foo = null;
+
+ var o = URL.createObjectURL(ms);
+ ok(o, "Create an objectURL from the MediaSource");
+
+ var v = document.createElement("video");
+ v.preload = "auto";
+ v.src = o;
+ document.body.appendChild(v);
+
+ var loadedmetadataCount = 0;
+ var updatestartCount = 0;
+ var updateendCount = 0;
+ var updateCount = 0;
+
+ ms.addEventListener("sourceopen", function () {
+ ok(true, "Receive a sourceopen event");
+ is(ms.readyState, "open", "MediaSource must be in open state after sourceopen");
+ var sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+ is(ms.sourceBuffers.length, 1, "MediaSource.sourceBuffers is expected length");
+ is(ms.sourceBuffers[0], sb, "SourceBuffer in list matches our SourceBuffer");
+ is(ms.activeSourceBuffers.length, 0, "MediaSource.activeSourceBuffers is expected length");
+
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ is(sb.updating, true, "SourceBuffer.updating is expected value after appendBuffer");
+ });
+
+ sb.addEventListener("update", function () {
+ is(sb.updating, false, "SourceBuffer.updating is expected value in update event");
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ }
+ });
+
+ sb.addEventListener("updatestart", function () {
+ updatestartCount++;
+ });
+
+ sb.addEventListener("updateend", function () {
+ is(ms.activeSourceBuffers[0], sb, "SourceBuffer in active list matches our SourceBuffer");
+ is(sb.updating, false, "SourceBuffer.updating is expected value in updateend event");
+ updateendCount++;
+ v.play();
+ });
+ });
+
+ ms.addEventListener("sourceended", function () {
+ ok(true, "Receive a sourceended event");
+ is(ms.readyState, "ended", "MediaSource must be in ended state after sourceended");
+ });
+
+ v.addEventListener("loadedmetadata", function () {
+ loadedmetadataCount++;
+ });
+
+ v.addEventListener("ended", function () {
+ // The bipbop video doesn't start at 0. The old MSE code adjust the
+ // timestamps and ignore the audio track. The new one doesn't.
+ isfuzzy(v.duration, 1.696, 0.166, "Video has correct duration");
+ isfuzzy(v.currentTime, 1.696, 0.166, "Video has correct duration");
+ // XXX: 2 update events can be received dueto duration differences, see bug 1065207.
+ ok(updateCount == 1 || updateCount == 2, "update event received");
+ ok(updateendCount == 1 || updateendCount == 2, "updateend event received");
+ ok(updatestartCount == 1 || updatestartCount == 2, "updatestart event received");
+ is(loadedmetadataCount, 1, "loadedmetadata event received");
+ v.parentNode.removeChild(v);
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MultipleInitSegments.html b/dom/media/mediasource/test/test_MultipleInitSegments.html
new file mode 100644
index 000000000..d97d72dae
--- /dev/null
+++ b/dom/media/mediasource/test/test_MultipleInitSegments.html
@@ -0,0 +1,53 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Append buffer with multiple init segments</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+ fetchWithXHR("seek_lowres.webm", function (seek_lowres) {
+ fetchWithXHR("seek.webm", function (seek) {
+ var data = [
+ [seek_lowres, 0, 438], // lowres init segment
+ [seek_lowres, 438, 25950], // lowres media segment 0-1
+ [seek, 0, 318], // init segment
+ [seek, 46712, 67833] // media segment 0.8-1.201
+ ];
+ var length = data.map(d => d[2] - d[1]).reduce((a, b) => a + b);
+ var arrayBuffer = new Uint8Array(length);
+ var pos = 0;
+ data.forEach(function(d) {
+ var buffer = new Uint8Array(d[0], d[1], d[2]-d[1]);
+ arrayBuffer.set(buffer, pos);
+ pos += buffer.byteLength;
+ });
+ loadSegment.bind(null, sb, arrayBuffer)().then(function() {
+ // Since we are passing multiple segments in one buffer,
+ // the first durationchange event from parsing the init
+ // segment will be fired before updateend.
+ v.addEventListener("durationchange", function () {
+ ok(v.duration, 1.201);
+ SimpleTest.finish();
+ });
+ ms.endOfStream();
+ });
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_MultipleInitSegments_mp4.html b/dom/media/mediasource/test/test_MultipleInitSegments_mp4.html
new file mode 100644
index 000000000..8b1a8c7b7
--- /dev/null
+++ b/dom/media/mediasource/test/test_MultipleInitSegments_mp4.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Append buffer with multiple init segments</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+ fetchWithXHR("bipbop/bipbop_videoinit.mp4", function (init) {
+ init = new Uint8Array(init);
+ fetchWithXHR("bipbop/bipbop_video1.m4s", function (segment1) {
+ segment1 = new Uint8Array(segment1);
+ fetchWithXHR("bipbop/bipbop_video2.m4s", function (segment2) {
+ segment2 = new Uint8Array(segment2);
+ var data = [init, segment1, init, segment2];
+ var length = data.map(d => d.byteLength).reduce((a, b) => a + b);
+ var arrayBuffer = new Uint8Array(length);
+ var pos = 0;
+ data.forEach(function(buffer) {
+ arrayBuffer.set(buffer, pos);
+ pos += buffer.byteLength;
+ });
+ loadSegment.bind(null, sb, arrayBuffer)().then(function() {
+ // Since we are passing multiple segments in one buffer,
+ // the first durationchange event from parsing the init
+ // segment will be fired before updateend.
+ v.addEventListener("durationchange", function () {
+ ok(v.duration, 1.601666);
+ SimpleTest.finish();
+ });
+ ms.endOfStream();
+ });
+ });
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_OnEvents.html b/dom/media/mediasource/test/test_OnEvents.html
new file mode 100644
index 000000000..1290e33f1
--- /dev/null
+++ b/dom/media/mediasource/test/test_OnEvents.html
@@ -0,0 +1,65 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: live seekable range</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ function initEvent(e) {
+ v['got' + e] = false;
+ }
+ function receiveEvent(e) {
+ v['got' + e] = true;
+ }
+ var msevents = ["onsourceopen", "onsourceended"];
+ msevents.forEach(function(e) {
+ initEvent(e);
+ ms[e] = function() { receiveEvent(e); };
+ });
+
+ var sblistevents = ["onaddsourcebuffer", "onremovesourcebuffer"];
+ sblistevents.forEach(function(e) {
+ initEvent(e);
+ ms.sourceBuffers[e] = function() { receiveEvent(e); };
+ });
+
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ var sbevents = ["onupdatestart", "onupdate", "onupdateend", "onabort"];
+ sbevents.forEach(function(e) {
+ initEvent(e);
+ sb[e] = function() { receiveEvent(e); };
+ });
+
+ fetchAndLoad(sb, 'seek', [''], '.webm')
+ .then(function() {
+ fetchWithXHR('seek.webm')
+ .then(function(arrayBuffer) {
+ sb.appendBuffer(arrayBuffer);
+ ms.removeSourceBuffer(sb); // will fire abort and removesourcebuffer
+ ms.endOfStream(); // will fire sourceended
+ once(ms, "sourceended").then(function() {
+ var events = ["onsourceopen", "onsourceended", "onupdatestart", "onupdate", "onupdateend", "onabort", "onaddsourcebuffer", "onremovesourcebuffer"];
+ events.forEach(function(e) {
+ ok(v['got' + e], "got " + e);
+ });
+ SimpleTest.finish();
+ });
+ });
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_PlayEvents.html b/dom/media/mediasource/test/test_PlayEvents.html
new file mode 100644
index 000000000..a390ae247
--- /dev/null
+++ b/dom/media/mediasource/test/test_PlayEvents.html
@@ -0,0 +1,165 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// This test checks that readyState is properly set and the appropriate events are being fired accordingly:
+// 1. Load 1.6s of data and ensure that canplay event is fired.
+// 2. Load data to have a complete buffered range from 0 to duration and ensure that canplaythrough is fired.
+// 3. Seek to an area with no buffered data, and ensure that readyState goes back to HAVE_METADATA
+// 4. Load 1.6s of data at the seek position and ensure that canplay is fired and that readyState is now HAVE_FUTURE_DATA
+// 5. Start playing video and check that once it reaches a position with no data, readyState goes back to HAVE_CURRENT_DATA and waiting event is fired.
+// 6. Add 1.6s of data once video element fired waiting, that canplay is fired once readyState is HAVE_FUTURE_DATA.
+// 7. Finally load data to the end and ensure that canplaythrough is fired and that readyState is now HAVE_ENOUGH_DATA
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ // Log events for debugging.
+ var events = ["suspend", "play", "canplay", "canplaythrough", "loadstart", "loadedmetadata",
+ "loadeddata", "playing", "ended", "error", "stalled", "emptied", "abort",
+ "waiting", "pause", "durationchange", "seeking", "seeked"];
+ function logEvent(e) {
+ var v = e.target;
+ info("got " + e.type + " event");
+ }
+ events.forEach(function(e) {
+ el.addEventListener(e, logEvent, false);
+ });
+
+ ok(true, "Receive a sourceopen event");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ el.addEventListener("error", function(e) {
+ ok(false, "should not fire '" + e + "' event");
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(once.bind(null, el, 'loadedmetadata'))
+ .then(function() {
+ ok(true, "got loadedmetadata event");
+ var promises = [];
+ promises.push(once(el, 'loadeddata'));
+ promises.push(once(el, 'canplay'));
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(1, 3), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got canplay event");
+ // set element duration to 3.203333s. We do so in order to guarantee that
+ // the end of the buffered range will be equal to duration, causing
+ // canplaythrough to be fired later.
+ ms.duration = 3.203333;
+ return once(el, 'durationchange');
+ })
+ .then(function() {
+ ok(true, "got durationchange event");
+ var promises = [];
+ promises.push(once(el, 'canplaythrough'));
+ // Load [0.801666, 3.203333]
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(3, 5), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got canplaythrough event");
+ // set element duration to 9.203333s, this value is set to coincide with
+ // data added later (we now have an empty range from 6s to 9.203333s).
+ ms.duration = 9.203333;
+ return once(el, 'durationchange');
+ })
+ .then(function() {
+ ok(true, "got durationchange event");
+ // An arbitrary value, so we are guaranteed to be in a range with no data.
+ el.currentTime = 6;
+ videosb.timestampOffset = 6;
+ ok(el.seeking, "seeking started");
+ return once(el, 'seeking');
+ })
+ .then(function() {
+ ok(true, "got seeking event");
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ var promises = [];
+ promises.push(once(el, 'seeked'));
+ promises.push(once(el, 'canplay'));
+ // Load [6+0, 6+1.601666)
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(1, 3), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got seeked and canplay event");
+ is(el.currentTime, 6, "seeked to 6s");
+ is(el.readyState, el.HAVE_FUTURE_DATA, "readyState is HAVE_FUTURE_DATA");
+ var promises = [];
+ promises.push(once(el, 'canplaythrough'));
+ // Load [6+1.60166, 6+3.203333]
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(3, 5), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got canplaythrough event");
+ // set element duration to 19.805s, this value is set to coincide with
+ // data added later (we now have an empty range from 15 to 19.805).
+ ms.duration = 19.805;
+ return once(el, 'durationchange');
+ })
+ .then(function() {
+ ok(true, "got durationchange event");
+ el.currentTime = 15;
+ videosb.timestampOffset = 15;
+ ok(el.seeking, "seeking started");
+ return once(el, 'seeking');
+ })
+ .then(function() {
+ ok(true, "got seeking event");
+ var promises = [];
+ promises.push(once(el, 'seeked'));
+ // Load [15+0, 15+1.601666)
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(1, 3), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got seeked event");
+ // Load [15+1.60166, 15+3.203333]
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(3, 5), '.m4s');
+ })
+ .then(function() {
+ ok(true, "data loaded");
+ // Playback we play for a little while then stall.
+ var promises = [];
+ promises.push(once(el, 'playing'));
+ promises.push(once(el, 'waiting'));
+ el.play();
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got playing and waiting event");
+ // Playback has stalled, readyState is back to HAVE_CURRENT_DATA.
+ is(el.readyState, el.HAVE_CURRENT_DATA, "readyState is HAVE_CURRENT_DATA");
+ var promises = [];
+ promises.push(once(el, 'playing'));
+ promises.push(once(el, 'canplay'));
+ promises.push(once(el, 'canplaythrough'));
+ // Load [15+3.203333, 15+4.805)
+ // Our final buffered range will now be [0, 3.203333)[6, 9.203333)[15, 19.805)
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(5, 7), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "got playing, canplay and canplaythrough event");
+ SimpleTest.finish();
+ })
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html b/dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html
new file mode 100644
index 000000000..b6769cb1b
--- /dev/null
+++ b/dom/media/mediasource/test/test_ResumeAfterClearing_mp4.html
@@ -0,0 +1,55 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var receivedSourceOpen = false;
+runWithMSE(function(ms, v) {
+ ms.addEventListener("sourceopen", function() {
+ ok(true, "Receive a sourceopen event");
+ ok(!receivedSourceOpen, "Should only receive one sourceopen for this test");
+ receivedSourceOpen = true;
+ var sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+
+ sb.addEventListener('error', (e) => { ok(false, "Got Error: " + e); SimpleTest.finish(); });
+ fetchAndLoad(sb, 'bipbop/bipbop', ['init'], '.mp4')
+ .then(function() {
+ var promises = [];
+ promises.push(fetchAndLoad(sb, 'bipbop/bipbop', range(1,3), '.m4s'));
+ promises.push(once(v, "loadeddata"));
+ return Promise.all(promises);
+ }).then(function() {
+ // clear the entire sourcebuffer.
+ sb.remove(0, 5);
+ return once(sb, "updateend");
+ }).then(function() {
+ v.play();
+ // We have nothing to play, waiting will be fired.
+ return once(v, "waiting");
+ }).then(function() {
+ var promises = [];
+ promises.push(once(v, "playing"));
+ promises.push(fetchAndLoad(sb, 'bipbop/bipbop', range(1,4), '.m4s'));
+ return Promise.all(promises);
+ }).then(function() {
+ ms.endOfStream();
+ var promises = [];
+ promises.push(once(ms, "sourceended"));
+ promises.push(once(v, "ended"));
+ return Promise.all(promises);
+ }).then(SimpleTest.finish.bind(SimpleTest));
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekNoData_mp4.html b/dom/media/mediasource/test/test_SeekNoData_mp4.html
new file mode 100644
index 000000000..efebff954
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekNoData_mp4.html
@@ -0,0 +1,69 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Avoid making trouble for people who fix rounding bugs.
+function fuzzyEquals(a, b) {
+ return Math.abs(a - b) < 0.01;
+}
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ el.addEventListener("error", function(e) {
+ ok(false, "should not fire '" + e + "' event");
+ });
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ try {
+ el.currentTime = 3;
+ } catch (e) {
+ ok(false, "should not throw '" + e + "' exception");
+ }
+ is(el.currentTime, 3, "currentTime is default playback start position");
+ is(el.seeking, false, "seek not started with HAVE_NOTHING");
+ fetchAndLoad(audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', ['init'], '.mp4'))
+ .then(once.bind(null, el, 'loadedmetadata'))
+ .then(function() {
+ var p = once(el, 'seeking');
+ el.play();
+ el.currentTime = 5;
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ is(el.seeking, true, "seek not started with HAVE_METADATA");
+ is(el.currentTime, 5, "currentTime is seek position");
+ return p;
+ })
+ .then(function() {
+ ok(true, "Got seeking event");
+ var promises = [];
+ promises.push(once(el, 'seeked'));
+ promises.push(fetchAndLoad(audiosb, 'bipbop/bipbop_audio', range(5, 9), '.m4s'));
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', range(6, 10), '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "Got seeked event");
+ ok(el.currentTime >= 5, "Time >= 5");
+ once(el, 'ended').then(SimpleTest.finish.bind(SimpleTest));
+ ms.endOfStream();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekToEnd_mp4.html b/dom/media/mediasource/test/test_SeekToEnd_mp4.html
new file mode 100644
index 000000000..2ffa53898
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekToEnd_mp4.html
@@ -0,0 +1,57 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seeking to end of data with data gap.</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 6), '.m4s'))
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4'))
+ .then(function() {
+ is(videosb.buffered.length, 1, "continuous buffered range");
+ // Ensure we have at least 2s less audio than video.
+ audiosb.appendWindowEnd = videosb.buffered.end(0) - 2;
+ return fetchAndLoad(audiosb, 'bipbop/bipbop_audio', range(1, 6), '.m4s');
+ }).then(function() {
+ ms.endOfStream();
+ return Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ }).then(function() {
+ ok(true, "endOfStream completed");
+ // Seek to the middle of the gap where audio is missing. As we are in readyState = ended
+ // seeking must complete.
+ el.currentTime = videosb.buffered.end(0) / 2 + audiosb.buffered.end(0) / 2;
+ ok(el.currentTime - audiosb.buffered.end(0) > 1, "gap is big enough");
+ is(el.buffered.length, 1, "continuous buffered range");
+ is(el.buffered.end(0), videosb.buffered.end(0), "buffered range end is aligned with longest track");
+ ok(el.seeking, "element is now seeking");
+ ok(el.currentTime >= el.buffered.start(0) && el.currentTime <= el.buffered.end(0), "seeking time is in buffered range");
+ ok(el.currentTime > audiosb.buffered.end(0), "seeking point is not buffered in audio track");
+ return once(el, 'seeked');
+ }).then(function() {
+ ok(true, "we have successfully seeked");
+ // Now ensure that we can play to the end, even though we are missing data in one track.
+ el.play();
+ once(el, 'ended').then(SimpleTest.finish.bind(SimpleTest));
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekTwice_mp4.html b/dom/media/mediasource/test/test_SeekTwice_mp4.html
new file mode 100644
index 000000000..dd250185f
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekTwice_mp4.html
@@ -0,0 +1,54 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// Avoid making trouble for people who fix rounding bugs.
+function fuzzyEquals(a, b) {
+ return Math.abs(a - b) < 0.01;
+}
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ fetchAndLoad(audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', range(1, 5), '.m4s'))
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', range(6, 12), '.m4s'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', ['init'], '.mp4'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 6), '.m4s'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(7, 14), '.m4s'))
+ .then(function() {
+ var p = once(el, 'seeking');
+ el.play();
+ el.currentTime = 4.5; // Seek to a gap in the video
+ return p;
+ }).then(function() {
+ ok(true, "Got seeking event");
+ var p = once(el, 'seeked');
+ el.currentTime = 6; // Seek past the gap.
+ return p;
+ }).then(function() {
+ ok(true, "Got seeked event");
+ ok(el.currentTime >= 6, "Time >= 6");
+ once(el, 'ended').then(SimpleTest.finish.bind(SimpleTest));
+ ms.endOfStream();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableAfterEndOfStream.html b/dom/media/mediasource/test/test_SeekableAfterEndOfStream.html
new file mode 100644
index 000000000..850434429
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableAfterEndOfStream.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ };
+ });
+ });
+
+ var target = 2;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit.html b/dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit.html
new file mode 100644
index 000000000..3aad2a0af
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream with split appendBuffer</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ // 25523 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25523));
+ var updateCount = 0;
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ if (updateCount == 1) {
+ // 25523 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25523));
+ }
+ else if (updateCount == 2) {
+ ms.endOfStream();
+ }
+ });
+ });
+
+ var target = 2;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit_mp4.html b/dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit_mp4.html
new file mode 100644
index 000000000..0933087a7
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableAfterEndOfStreamSplit_mp4.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream with split appendBuffer</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25819));
+ var updateCount = 0;
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ if (updateCount == 1) {
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25819));
+ }
+ else if (updateCount == 2) {
+ ms.endOfStream();
+ }
+ });
+ });
+
+ var target = 1.3;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableAfterEndOfStream_mp4.html b/dom/media/mediasource/test/test_SeekableAfterEndOfStream_mp4.html
new file mode 100644
index 000000000..7adadad85
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableAfterEndOfStream_mp4.html
@@ -0,0 +1,49 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute after end of stream</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ /* Ensure that we endOfStream on the first update event only as endOfStream can
+ raise more if the duration of the last buffered range and the intial duration
+ differ. See bug 1065207 */
+ if (updateCount == 1) {
+ ms.endOfStream();
+ };
+ });
+ });
+
+ var target = 1.3;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeEndOfStream.html b/dom/media/mediasource/test/test_SeekableBeforeEndOfStream.html
new file mode 100644
index 000000000..49401d213
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeEndOfStream.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ });
+
+ var target = 2;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit.html b/dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit.html
new file mode 100644
index 000000000..2013fb4e6
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit.html
@@ -0,0 +1,42 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream with split appendBuffer</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25523));
+ sb.addEventListener("updateend", function () {
+ sb.removeEventListener('updateend', arguments.callee);
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25523));
+ });
+ });
+
+ var target = 2;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit_mp4.html b/dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit_mp4.html
new file mode 100644
index 000000000..f14669665
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeEndOfStreamSplit_mp4.html
@@ -0,0 +1,44 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream with split appendBuffer</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 25819));
+ sb.addEventListener("updateend", function () {
+ sb.removeEventListener('updateend', arguments.callee);
+ // 25819 is the offset of the first media segment's end
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 25819));
+ });
+ });
+
+ var target = 1.3;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekableBeforeEndOfStream_mp4.html b/dom/media/mediasource/test/test_SeekableBeforeEndOfStream_mp4.html
new file mode 100644
index 000000000..cc06ff00d
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekableBeforeEndOfStream_mp4.html
@@ -0,0 +1,38 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: seekable attribute before end of stream</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ });
+
+ var target = 1.3;
+
+ v.addEventListener("loadedmetadata", function () {
+ ok(v.seekable.length, "Resource is seekable");
+ ok(v.seekable.length &&
+ target >= v.seekable.start(0) &&
+ target < v.seekable.end(0), "Target is within seekable range");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SeekedEvent_mp4.html b/dom/media/mediasource/test/test_SeekedEvent_mp4.html
new file mode 100644
index 000000000..f80ae86f5
--- /dev/null
+++ b/dom/media/mediasource/test/test_SeekedEvent_mp4.html
@@ -0,0 +1,75 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: Check that seeked event is fired prior loadeddata</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ el._seeked = false;
+ el._loadeddata = false;
+ el._playing = false;
+ el.addEventListener("seeked", function() {
+ ok(true, "got seeked event");
+ is(el._loadeddata, false, "can't have received loadeddata prior seeked");
+ is(el._playing, false, "can't be playing prior seeked");
+ el._seeked = true;
+ });
+ el.addEventListener("loadeddata", function() {
+ ok(true, "got loadeddata event");
+ is(el._seeked, true, "must have received seeked prior loadeddata");
+ is(el._playing, false, "can't be playing prior playing");
+ el._loadeddata = true;
+ });
+ el.addEventListener("playing", function() {
+ ok(true, "got playing");
+ is(el._seeked, true, "must have received seeked prior playing");
+ is(el._loadeddata, true, "must have received loadeddata prior playing");
+ el._playing = true;
+ });
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ is(el.readyState, el.HAVE_NOTHING, "readyState is HAVE_NOTHING");
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(once.bind(null, el, "loadedmetadata"))
+ .then(function() {
+ el.play();
+ videosb.timestampOffset = 2;
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ // Load [2, 3.606).
+ var promises = [];
+ promises.push(once(el, "play"));
+ promises.push(fetchAndLoad(videosb, 'bipbop/bipbop_video', ['1'], '.m4s'));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', ['2'], '.m4s');
+ })
+ .then(function() {
+ is(el.readyState, el.HAVE_METADATA, "readyState is HAVE_METADATA");
+ el.currentTime = 2;
+ var promises = [];
+ promises.push(once(el, "seeked"));
+ promises.push(once(el, "playing"));
+ return Promise.all(promises);
+ })
+ .then(function() {
+ ok(true, "completed seek");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Sequence_mp4.html b/dom/media/mediasource/test/test_Sequence_mp4.html
new file mode 100644
index 000000000..a68393547
--- /dev/null
+++ b/dom/media/mediasource/test/test_Sequence_mp4.html
@@ -0,0 +1,39 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: Don't get stuck buffering for too long when we have frames to show</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var receivedSourceOpen = false;
+runWithMSE(function(ms, v) {
+ ms.addEventListener("sourceopen", function() {
+ ok(true, "Receive a sourceopen event");
+ ok(!receivedSourceOpen, "Should only receive one sourceopen for this test");
+ receivedSourceOpen = true;
+ var sb = ms.addSourceBuffer("video/mp4");
+ ok(sb, "Create a SourceBuffer");
+ sb.addEventListener('error', (e) => { ok(false, "Got Error: " + e); SimpleTest.finish(); });
+ sb.mode = 'sequence';
+
+ fetchAndLoad(sb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, sb, 'bipbop/bipbop_video', ['5'], '.m4s'))
+ .then(fetchAndLoad.bind(null, sb, 'bipbop/bipbop_video', ['2'], '.m4s'))
+ .then(function() {
+ is(v.buffered.length, 1, "Continuous buffered range");
+ is(v.buffered.start(0), 0, "Buffered range starts at 0");
+ ok(sb.timestampOffset > 0, "SourceBuffer.timestampOffset set to allow continuous range");
+ SimpleTest.finish();
+ });
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SetModeThrows.html b/dom/media/mediasource/test/test_SetModeThrows.html
new file mode 100644
index 000000000..ce8f955cd
--- /dev/null
+++ b/dom/media/mediasource/test/test_SetModeThrows.html
@@ -0,0 +1,34 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+// MSE supports setting mode now. make sure it does not throw.
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ sb.mode = "segments";
+ ok("true", "Setting to segments does not throw");
+ try {
+ sb.mode = "sequence";
+ ok("true", "Setting to sequence does not throw");
+ } catch (e) { ok(false, "Should not throw setting mode to sequence: " + e); }
+
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppend.html b/dom/media/mediasource/test/test_SplitAppend.html
new file mode 100644
index 000000000..e87bd00ed
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppend.html
@@ -0,0 +1,47 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization and media segment separately</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ if (updateCount == 1) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ }
+ else if (updateCount == 2) {
+ ms.endOfStream();
+ }
+ });
+ v.play();
+ });
+ });
+
+ v.addEventListener("ended", function () {
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ ok(Math.abs(v.currentTime - 4) <= 0.002, "Video has played to end");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppendDelay.html b/dom/media/mediasource/test/test_SplitAppendDelay.html
new file mode 100644
index 000000000..c5d443e80
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppendDelay.html
@@ -0,0 +1,50 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append segments with delay</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 318));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ if (updateCount == 1) {
+ window.setTimeout(function () {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 318));
+ }, 1000);
+ }
+ else if (updateCount == 2) {
+ ms.endOfStream();
+ }
+ });
+ v.play();
+ });
+ });
+
+ v.addEventListener("ended", function () {
+ // XXX: Duration should be exactly 4.0, see bug 1065207.
+ ok(Math.abs(v.duration - 4) <= 0.002, "Video has correct duration");
+ ok(Math.abs(v.currentTime - 4) <= 0.002, "Video has played to end");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppendDelay_mp4.html b/dom/media/mediasource/test/test_SplitAppendDelay_mp4.html
new file mode 100644
index 000000000..d48e63324
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppendDelay_mp4.html
@@ -0,0 +1,51 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append segments with delay</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+SimpleTest.requestFlakyTimeout("untriaged");
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ if (updateCount == 1) {
+ window.setTimeout(function () {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 1395));
+ }, 1000);
+ }
+ else if (updateCount == 2) {
+ ms.endOfStream();
+ }
+ });
+ v.play();
+ });
+ });
+
+ v.addEventListener("ended", function () {
+ // The bipbop video doesn't start at 0. The old MSE code adjust the
+ // timestamps and ignore the audio track. The new one doesn't.
+ isfuzzy(v.duration, 1.696, 0.166, "Video has correct duration");
+ isfuzzy(v.currentTime, 1.696, 0.166, "Video has played to end");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_SplitAppend_mp4.html b/dom/media/mediasource/test/test_SplitAppend_mp4.html
new file mode 100644
index 000000000..9e006e699
--- /dev/null
+++ b/dom/media/mediasource/test/test_SplitAppend_mp4.html
@@ -0,0 +1,48 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: append initialization and media segment separately</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+var updateCount = 0;
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 0, 1395));
+ sb.addEventListener("updateend", function () {
+ updateCount++;
+ if (updateCount == 1) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer, 1395));
+ }
+ else if (updateCount == 2) {
+ ms.endOfStream();
+ }
+ });
+ v.play();
+ });
+ });
+
+ v.addEventListener("ended", function () {
+ // The bipbop video doesn't start at 0. The old MSE code adjust the
+ // timestamps and ignore the audio track. The new one doesn't.
+ isfuzzy(v.duration, 1.696, 0.166, "Video has correct duration");
+ isfuzzy(v.currentTime, 1.696, 0.166, "Video has played to end");
+ SimpleTest.finish();
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_Threshold_mp4.html b/dom/media/mediasource/test/test_Threshold_mp4.html
new file mode 100644
index 000000000..a33dc08ec
--- /dev/null
+++ b/dom/media/mediasource/test/test_Threshold_mp4.html
@@ -0,0 +1,84 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: data gap detection</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+
+var threshold = 0.5; // gap threshold in seconds.
+var fuzz = 0.000001; // fuzz when comparing double.
+
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ var vchunks = [ {start: 0, end: 3.203333}, { start: 3.203333, end: 6.406666}];
+
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 5), '.m4s'))
+ .then(function() {
+ // We will insert a gap of threshold
+ videosb.timestampOffset = threshold;
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(5, 9), '.m4s');
+ }).then(function() {
+ // HTMLMediaElement fires 'waiting' if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get 'playing' before
+ // we starting waiting for 'waiting'.
+ info("Invoking play()");
+ var p = once(el, 'playing');
+ el.play();
+ return p;
+ }).then(function() {
+ return once(el, 'waiting');
+ }).then(function() {
+ // We're waiting for data after the start of the last frame.
+ // 0.033333 is the duration of the last frame.
+ ok(el.currentTime >= vchunks[1].end - 0.033333 + threshold - fuzz
+ && el.currentTime <= vchunks[1].end + threshold + fuzz, "skipped the gap properly: " + el.currentTime + " " + (vchunks[1].end + threshold));
+ is(el.buffered.length, 2, "buffered range has right length");
+ // Now we test that seeking will succeed despite the gap.
+ el.currentTime = el.buffered.end(0) + (threshold / 2);
+ return once(el, 'seeked');
+ }).then(function() {
+ // Now we test that we don't pass the gap.
+ // Clean up our sourcebuffer by removing all data.
+ videosb.timestampOffset = 0;
+ videosb.remove(0, Infinity);
+ el.currentTime = 0;
+ el.pause();
+ return once(videosb, "updateend");
+ }).then(function() {
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(1, 5), '.m4s');
+ }).then(function() {
+ // We will insert a gap of threshold + 1ms
+ videosb.timestampOffset = threshold + 1/1000;
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(5, 9), '.m4s');
+ }).then(function() {
+ info("Invoking play()");
+ var p = once(el, 'playing');
+ el.play();
+ return p;
+ }).then(function() {
+ return once(el, 'waiting');
+ }).then(function() {
+ // We're waiting for data after the start of the last frame.
+ // 0.033333 is the duration of the last frame.
+ ok(el.currentTime >= vchunks[0].end - 0.033333 - fuzz
+ && el.currentTime <= vchunks[0].end + fuzz, "stopped at the gap properly: " + el.currentTime + " " + vchunks[0].end);
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_TimestampOffset_mp4.html b/dom/media/mediasource/test/test_TimestampOffset_mp4.html
new file mode 100644
index 000000000..2a80e3e5c
--- /dev/null
+++ b/dom/media/mediasource/test/test_TimestampOffset_mp4.html
@@ -0,0 +1,86 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: basic functionality</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+function range(start, end) {
+ var rv = [];
+ for (var i = start; i < end; ++i) {
+ rv.push(i);
+ }
+ return rv;
+}
+
+var eps = 0.01;
+runWithMSE(function(ms, el) {
+
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ // We divide the video into 3 chunks:
+ // chunk 0: segments 1-4
+ // chunk 1: segments 5-8
+ // chunk 2: segments 9-13
+ // We then fill the timeline so that it seamlessly plays the chunks in order 0, 2, 1.
+ var vchunks = [ {start: 0, end: 3.2033}, { start: 3.2033, end: 6.4066}, { start: 6.4066, end: 10.01} ];
+ var firstvoffset = vchunks[2].end - vchunks[2].start; // Duration of chunk 2
+ var secondvoffset = -(vchunks[1].end - vchunks[1].start); // -(Duration of chunk 1)
+
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 5), '.m4s'))
+ .then(function() {
+ is(videosb.buffered.length, 1, "No discontinuity");
+ isfuzzy(videosb.buffered.start(0), vchunks[0].start, eps, "Chunk start");
+ isfuzzy(videosb.buffered.end(0), vchunks[0].end, eps, "Chunk end");
+ videosb.timestampOffset = firstvoffset;
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(5, 9), '.m4s');
+ })
+ .then(function(data) {
+ is(videosb.buffered.length, 2, "One discontinuity");
+ isfuzzy(videosb.buffered.start(0), vchunks[0].start, eps, "First Chunk start");
+ isfuzzy(videosb.buffered.end(0), vchunks[0].end, eps, "First chunk end");
+ isfuzzy(videosb.buffered.start(1), vchunks[1].start + firstvoffset, eps, "Second chunk start");
+ isfuzzy(videosb.buffered.end(1), vchunks[1].end + firstvoffset, eps, "Second chunk end");
+ videosb.timestampOffset = secondvoffset;
+ return fetchAndLoad(videosb, 'bipbop/bipbop_video', range(9, 14), '.m4s');
+ })
+ .then(function() {
+ is(videosb.buffered.length, 1, "No discontinuity (end)");
+ isfuzzy(videosb.buffered.start(0), vchunks[0].start, eps, "Chunk start");
+ isfuzzy(videosb.buffered.end(0), vchunks[2].end, eps, "Chunk end");
+ audiosb.timestampOffset = 3;
+ }).then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4'))
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', range(1, 12), '.m4s'))
+ .then(function() {
+ is(audiosb.buffered.length, 1, "No audio discontinuity");
+ isfuzzy(audiosb.buffered.start(0), 3, eps, "Audio starts at 3");
+
+ // Trim the rest of the audio.
+ audiosb.remove(videosb.buffered.end(0), Infinity);
+ videosb.remove(videosb.buffered.end(0), Infinity);
+ return Promise.all([audiosb.updating ? once(audiosb, 'updateend') : Promise.resolve(),
+ videosb.updating ? once(videosb, 'updateend') : Promise.resolve()]);
+ }).then(function() {
+ info("waiting for play to complete");
+ el.play();
+ el.currentTime = el.buffered.start(0);
+ ms.endOfStream();
+ Promise.all([once(el, 'ended'), once(el, 'seeked')]).then(SimpleTest.finish.bind(SimpleTest));
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_TruncatedDuration.html b/dom/media/mediasource/test/test_TruncatedDuration.html
new file mode 100644
index 000000000..cf25de395
--- /dev/null
+++ b/dom/media/mediasource/test/test_TruncatedDuration.html
@@ -0,0 +1,78 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: truncating the media seeks to end of media and update buffered range</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// This test append data to a mediasource and then seek to half the duration
+// of the video.
+// We then shorten the video to 1/3rd of its original size by modifying the
+// mediasource.duration attribute.
+// We ensure that the buffered range immediately reflect the truncation
+// and that we've seeked to the new end of the media as per W3C spec and
+// video.currentTime got updated.
+
+SimpleTest.waitForExplicitFinish();
+
+function round(n) {
+ return Math.round(n * 1000) / 1000;
+}
+
+function do_seeking(e) {
+ var v = e.target;
+ v.removeEventListener("seeking", do_seeking, false);
+ SimpleTest.finish();
+}
+
+function do_seeked(e) {
+ var v = e.target;
+ v.removeEventListener("seeked", do_seeked, false);
+ var duration = round(v.duration / 3);
+ is(v._sb.updating, false, "sourcebuffer isn't updating");
+ v._sb.remove(duration, Infinity);
+ once(v._sb, "updateend", function() {
+ v._ms.duration = duration
+ // frames aren't truncated, so duration may be slightly more.
+ isfuzzy(v.duration, duration, 1/30, "element duration was updated");
+ v._sb.abort(); // this shouldn't abort updating the duration (bug 1130826).
+ ok(v.seeking, "seeking is true");
+ // test playback position was updated (bug 1130839).
+ is(v.currentTime, v.duration, "current time was updated");
+ is(v._sb.buffered.length, 1, "One buffered range");
+ // Truncated mediasource duration will cause the video element to seek.
+ v.addEventListener("seeking", do_seeking, false);
+ });
+}
+
+function do_loaded(e) {
+ var v = e.target;
+ v.removeEventListener("loadeddata", do_loaded, false);
+ v.currentTime = v.duration / 2;
+ is(v.currentTime, v.duration / 2, "current time was updated");
+ ok(v.seeking, "seeking is true");
+ v.addEventListener("seeked", do_seeked, false);
+}
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/webm");
+ v._sb = sb;
+ v._ms = ms;
+
+ fetchWithXHR("seek.webm", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ v.addEventListener("loadeddata", do_loaded, false);
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_TruncatedDuration_mp4.html b/dom/media/mediasource/test/test_TruncatedDuration_mp4.html
new file mode 100644
index 000000000..f226093de
--- /dev/null
+++ b/dom/media/mediasource/test/test_TruncatedDuration_mp4.html
@@ -0,0 +1,83 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>MSE: truncating the media seeks to end of media and update buffered range</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test">
+<script class="testbody" type="text/javascript">
+
+// This test append data to a mediasource and then seek to half the duration
+// of the video.
+// We then shorten the video to 1/3rd of its original size.
+// We ensure that the buffered range immediately reflect the truncation
+// and that we've seeked to the new end of the media as per W3C spec and
+// video.currentTime got updated.
+
+SimpleTest.waitForExplicitFinish();
+
+function round(n) {
+ return Math.round(n * 1000) / 1000;
+}
+
+function do_seeking(e) {
+ var v = e.target;
+ v.removeEventListener("seeking", do_seeking, false);
+ SimpleTest.finish();
+}
+
+function do_seeked(e) {
+ var v = e.target;
+ v.removeEventListener("seeked", do_seeked, false);
+ var duration = round(v.duration / 3);
+ is(v._sb.updating, false, "sourcebuffer isn't updating");
+ v._sb.remove(duration, Infinity);
+ once(v._sb, "updateend", function() {
+ v._ms.duration = duration
+ // frames aren't truncated, so duration may be slightly more.
+ isfuzzy(v.duration, duration, 1/30, "element duration was updated");
+ v._sb.abort(); // this shouldn't abort updating the duration (bug 1130826).
+ ok(v.seeking, "seeking is true");
+ // test playback position was updated (bug 1130839).
+ is(v.currentTime, v.duration, "current time was updated");
+ is(v._sb.buffered.length, 1, "One buffered range");
+ // Truncated mediasource duration will cause the video element to seek.
+ v.addEventListener("seeking", do_seeking, false);
+ });
+}
+
+function do_loaded(e) {
+ var v = e.target;
+ v.removeEventListener("loadeddata", do_loaded, false);
+ // mp4 metadata states 10s when we only have 1.6s worth of video.
+ v._sb.remove(v._sb.buffered.end(0), Infinity);
+ once(v._sb, "updateend", function() {
+ v._ms.duration = v._sb.buffered.end(0);
+ is(v.duration, v._ms.duration, "current time updated with mediasource duration");
+ v.currentTime = v.duration / 2;
+ is(v.currentTime, v.duration / 2, "current time was updated");
+ ok(v.seeking, "seeking is true");
+ v.addEventListener("seeked", do_seeked, false);
+ });
+}
+
+runWithMSE(function (ms, v) {
+ ms.addEventListener("sourceopen", function () {
+ var sb = ms.addSourceBuffer("video/mp4");
+ v._sb = sb;
+ v._ms = ms;
+
+ fetchWithXHR("bipbop/bipbop2s.mp4", function (arrayBuffer) {
+ sb.appendBuffer(new Uint8Array(arrayBuffer));
+ v.addEventListener("loadeddata", do_loaded, false);
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingOnMissingData.html b/dom/media/mediasource/test/test_WaitingOnMissingData.html
new file mode 100644
index 000000000..a67441045
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingOnMissingData.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var sb = ms.addSourceBuffer("video/webm");
+ fetchWithXHR("seek.webm", function(arrayBuffer) {
+ sb.addEventListener('error', (e) => { ok(false, "Got Error: " + e); SimpleTest.finish(); });
+ loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 0, 318))()
+ .then(loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 318, 25223-318)))
+ .then(loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 25223, 46712-25223)))
+ /* Note - Missing |46712, 67833 - 46712| segment here */
+ .then(loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 67833, 88966 - 67833)))
+ .then(loadSegment.bind(null, sb, new Uint8Array(arrayBuffer, 88966)))
+ .then(function() {
+ // HTMLMediaElement fires 'waiting' if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get 'playing' before
+ // we starting waiting for 'waiting'.
+ info("Invoking play()");
+ var p = once(el, 'playing');
+ el.play();
+ return p;
+ }).then(function() {
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ var p = once(el, 'waiting');
+ el.play();
+ return p;
+ }).then(function() {
+ // currentTime is based on the current video frame, so if the audio ends just before
+ // the next video frame, currentTime can be up to 1 frame's worth earlier than
+ // min(audioEnd, videoEnd).
+ // 0.0465 is the length of the last audio frame.
+ ok(el.currentTime >= (sb.buffered.end(0) - 0.0465),
+ "Got a waiting event at " + el.currentTime);
+ info("Loading more data");
+ var p = once(el, 'ended');
+ loadSegment(sb, new Uint8Array(arrayBuffer, 46712, 67833 - 46712)).then(() => ms.endOfStream());
+ return p;
+ }).then(function() {
+ // These fuzz factors are bigger than they should be. We should investigate
+ // and fix them in bug 1137574.
+ isfuzzy(el.duration, 4.001, 0.1, "Video has correct duration: " + el.duration);
+ isfuzzy(el.currentTime, el.duration, 0.1, "Video has correct currentTime.");
+ SimpleTest.finish();
+ });
+ });
+ });
+});
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html b/dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html
new file mode 100644
index 000000000..6e1560d01
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingOnMissingDataEnded_mp4.html
@@ -0,0 +1,53 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ el.addEventListener("ended", function () {
+ ok(false, "ended should never fire");
+ SimpleTest.finish();
+ });
+ var videosb = ms.addSourceBuffer("video/mp4");
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 5), '.m4s'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(6, 8), '.m4s'))
+ .then(function() {
+ is(el.buffered.length, 2, "discontinuous buffered range");
+ ms.endOfStream();
+ return Promise.all([once(el, "durationchange"), once(ms, "sourceended")]);
+ }).then(function() {
+ // HTMLMediaElement fires 'waiting' if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get 'playing' before
+ // we starting waiting for 'waiting'.
+ info("Invoking play()");
+ el.play();
+ return once(el, 'playing');
+ }).then(function() {
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ return once(el, 'waiting');
+ }).then(function() {
+ // waiting is fired when we start to play the last frame.
+ // 0.033334 is the duration of the last frame, + 0.000001 of fuzz.
+ // the next video frame, currentTime can be up to 1 frame's worth earlier than end of video.
+ isfuzzy(el.currentTime, videosb.buffered.end(0), 0.033334, "waiting was fired on gap");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html b/dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html
new file mode 100644
index 000000000..79344c772
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingOnMissingData_mp4.html
@@ -0,0 +1,65 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ fetchAndLoad(audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', range(1, 5), '.m4s'))
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', range(6, 12), '.m4s'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', ['init'], '.mp4'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 6), '.m4s'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(7, 14), '.m4s'))
+ .then(function() {
+ // HTMLMediaElement fires 'waiting' if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get 'playing' before
+ // we starting waiting for 'waiting'.
+ info("Invoking play()");
+ var p = once(el, 'playing');
+ el.play();
+ return p;
+ }).then(function() {
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ var p = once(el, 'waiting');
+ el.play();
+ return p;
+ }).then(function() {
+ // currentTime is based on the current video frame, so if the audio ends just before
+ // the next video frame, currentTime can be up to 1 frame's worth earlier than
+ // min(audioEnd, videoEnd).
+ // 0.0465 is the length of the last audio frame.
+ ok(el.currentTime >= (Math.min(audiosb.buffered.end(0), videosb.buffered.end(0)) - 0.0465),
+ "Got a waiting event at " + el.currentTime);
+ info("Loading more data");
+ var p = once(el, 'ended');
+ var loads = Promise.all([fetchAndLoad(audiosb, 'bipbop/bipbop_audio', [5], '.m4s'),
+ fetchAndLoad(videosb, 'bipbop/bipbop_video', [6], '.m4s')]);
+ loads.then(() => ms.endOfStream());
+ return p;
+ }).then(function() {
+ // These fuzz factors are bigger than they should be. We should investigate
+ // and fix them in bug 1137574.
+ isfuzzy(el.duration, 10.1, 0.1, "Video has correct duration: " + el.duration);
+ isfuzzy(el.currentTime, el.duration, 0.1, "Video has correct currentTime.");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html b/dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html
new file mode 100644
index 000000000..f932b3d4e
--- /dev/null
+++ b/dom/media/mediasource/test/test_WaitingToEndedTransition_mp4.html
@@ -0,0 +1,56 @@
+<!DOCTYPE html>
+<html><head>
+<meta http-equiv="content-type" content="text/html; charset=windows-1252">
+ <title>MSE: |waiting| event when source data is missing</title>
+ <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <script type="text/javascript" src="mediasource.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" />
+</head>
+<body>
+<pre id="test"><script class="testbody" type="text/javascript">
+
+SimpleTest.waitForExplicitFinish();
+
+runWithMSE(function(ms, el) {
+ el.controls = true;
+ once(ms, 'sourceopen').then(function() {
+ ok(true, "Receive a sourceopen event");
+ var audiosb = ms.addSourceBuffer("audio/mp4");
+ var videosb = ms.addSourceBuffer("video/mp4");
+ // ensure tracks end at approximately the same time to ensure ended event is
+ // always fired (bug 1233639).
+ audiosb.appendWindowEnd = 3.9;
+ videosb.appendWindowEnd = 3.9;
+ fetchAndLoad(audiosb, 'bipbop/bipbop_audio', ['init'], '.mp4')
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', ['init'], '.mp4'))
+ .then(fetchAndLoad.bind(null, audiosb, 'bipbop/bipbop_audio', range(1, 5), '.m4s'))
+ .then(fetchAndLoad.bind(null, videosb, 'bipbop/bipbop_video', range(1, 6), '.m4s'))
+ .then(function() {
+ // HTMLMediaElement fires 'waiting' if somebody invokes |play()| before the MDSM
+ // has notified it of available data. Make sure that we get 'playing' before
+ // we starting waiting for 'waiting'.
+ info("Invoking play()");
+ var p = once(el, 'playing');
+ el.play();
+ return p;
+ }).then(function() {
+ ok(true, "Video playing. It should play for a bit, then fire 'waiting'");
+ var p = once(el, 'waiting');
+ el.play();
+ return p;
+ }).then(function() {
+ var p = once(el, 'ended');
+ ms.endOfStream();
+ return p;
+ }).then(function() {
+ is(el.duration, 3.854512, "Video has correct duration: " + el.duration);
+ is(el.currentTime, el.duration, "Video has correct currentTime.");
+ SimpleTest.finish();
+ });
+ });
+});
+
+</script>
+</pre>
+</body>
+</html>