--- a/dom/locales/en-US/chrome/dom/dom.properties
+++ b/dom/locales/en-US/chrome/dom/dom.properties
@@ -102,16 +102,18 @@ MediaLoadInvalidURI=Invalid URI. Load of
# LOCALIZATION NOTE: %1$S is the media resource's format/codec type (basically equivalent to the file type, e.g. MP4,AVI,WMV,MOV etc), %2$S is the URL of the media resource which failed to load.
MediaLoadUnsupportedTypeAttribute=Specified "type" attribute of "%1$S" is not supported. Load of media resource %2$S failed.
# LOCALIZATION NOTE: %1$S is the "media" attribute value of the <source> element. It is a media query. %2$S is the URL of the media resource which failed to load.
MediaLoadSourceMediaNotMatched=Specified "media" attribute of "%1$S" does not match the environment. Load of media resource %2$S failed.
# LOCALIZATION NOTE: %1$S is the MIME type HTTP header being sent by the web server, %2$S is the URL of the media resource which failed to load.
MediaLoadUnsupportedMimeType=HTTP "Content-Type" of "%1$S" is not supported. Load of media resource %2$S failed.
# LOCALIZATION NOTE: %S is the URL of the media resource which failed to load because of error in decoding.
MediaLoadDecodeError=Media resource %S could not be decoded.
+# LOCALIZATION NOTE: Do not translate "MediaRecorder".
+MediaRecorderMultiTracksNotSupported=MediaRecorder does not support recording multiple tracks of the same type at this time.
# LOCALIZATION NOTE: %S is the ID of the MediaStreamTrack passed to MediaStream.addTrack(). Do not translate "MediaStreamTrack" and "AudioChannel".
MediaStreamAddTrackDifferentAudioChannel=MediaStreamTrack %S could not be added since it belongs to a different AudioChannel.
# LOCALIZATION NOTE: Do not translate "MediaStream", "stop()" and "MediaStreamTrack"
MediaStreamStopDeprecatedWarning=MediaStream.stop() is deprecated and will soon be removed. Use MediaStreamTrack.stop() instead.
# LOCALIZATION NOTE: Do not translate "DOMException", "code" and "name"
DOMExceptionCodeWarning=Use of DOMException's code attribute is deprecated. Use name instead.
# LOCALIZATION NOTE: Do not translate "__exposedProps__"
NoExposedPropsWarning=Exposing chrome JS objects to content without __exposedProps__ is insecure and deprecated. See https://developer.mozilla.org/en/XPConnect_wrappers for more information.
--- a/dom/media/MediaRecorder.cpp
+++ b/dom/media/MediaRecorder.cpp
@@ -18,16 +18,17 @@
#include "mozilla/dom/File.h"
#include "mozilla/dom/RecordErrorEvent.h"
#include "mozilla/dom/VideoStreamTrack.h"
#include "nsContentUtils.h"
#include "nsError.h"
#include "nsIDocument.h"
#include "nsIPermissionManager.h"
#include "nsIPrincipal.h"
+#include "nsIScriptError.h"
#include "nsMimeTypes.h"
#include "nsProxyRelease.h"
#include "nsTArray.h"
#include "GeckoProfiler.h"
#ifdef LOG
#undef LOG
#endif
@@ -149,17 +150,19 @@ NS_IMPL_RELEASE_INHERITED(MediaRecorder,
* Therefore, the reference dependency in gecko is:
* ShutdownObserver -> Session <-> MediaRecorder, note that there is a cycle
* reference between Session and MediaRecorder.
* 2) A Session is destroyed in DestroyRunnable after MediaRecorder::Stop being called
* _and_ all encoded media data been passed to OnDataAvailable handler.
* 3) MediaRecorder::Stop is called by user or the document is going to
* inactive or invisible.
*/
-class MediaRecorder::Session: public nsIObserver
+class MediaRecorder::Session: public nsIObserver,
+ public PrincipalChangeObserver<MediaStreamTrack>,
+ public DOMMediaStream::TrackListener
{
NS_DECL_THREADSAFE_ISUPPORTS
// Main thread task.
// Create a blob event and send back to client.
class PushBlobRunnable : public nsRunnable
{
public:
@@ -287,27 +290,61 @@ class MediaRecorder::Session: public nsI
// For Ensure recorder has tracks to record.
class TracksAvailableCallback : public OnTracksAvailableCallback
{
public:
explicit TracksAvailableCallback(Session *aSession)
: mSession(aSession) {}
virtual void NotifyTracksAvailable(DOMMediaStream* aStream)
{
+ if (mSession->mStopIssued) {
+ return;
+ }
+
+ MOZ_RELEASE_ASSERT(aStream);
+ mSession->MediaStreamReady(*aStream);
+
uint8_t trackTypes = 0;
nsTArray<RefPtr<mozilla::dom::AudioStreamTrack>> audioTracks;
aStream->GetAudioTracks(audioTracks);
if (!audioTracks.IsEmpty()) {
trackTypes |= ContainerWriter::CREATE_AUDIO_TRACK;
+ mSession->ConnectMediaStreamTrack(*audioTracks[0]);
}
nsTArray<RefPtr<mozilla::dom::VideoStreamTrack>> videoTracks;
aStream->GetVideoTracks(videoTracks);
if (!videoTracks.IsEmpty()) {
trackTypes |= ContainerWriter::CREATE_VIDEO_TRACK;
+ mSession->ConnectMediaStreamTrack(*videoTracks[0]);
+ }
+
+ if (audioTracks.Length() > 1 ||
+ videoTracks.Length() > 1) {
+ // When MediaRecorder supports multiple tracks, we should set up a single
+ // MediaInputPort from the input stream, and let main thread check
+ // track principals async later.
+ nsPIDOMWindowInner* window = mSession->mRecorder->GetParentObject();
+ nsIDocument* document = window ? window->GetExtantDoc() : nullptr;
+ nsContentUtils::ReportToConsole(nsIScriptError::errorFlag,
+ NS_LITERAL_CSTRING("Media"),
+ document,
+ nsContentUtils::eDOM_PROPERTIES,
+ "MediaRecorderMultiTracksNotSupported");
+ mSession->DoSessionEndTask(NS_ERROR_ABORT);
+ return;
+ }
+
+ NS_ASSERTION(trackTypes != 0, "TracksAvailableCallback without any tracks available");
+
+ // Check that we may access the tracks' content.
+ if (!mSession->MediaStreamTracksPrincipalSubsumes()) {
+ LOG(LogLevel::Warning, ("Session.NotifyTracksAvailable MediaStreamTracks principal check failed"));
+ mSession->DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+ return;
}
LOG(LogLevel::Debug, ("Session.NotifyTracksAvailable track type = (%d)", trackTypes));
mSession->InitEncoder(trackTypes);
}
private:
RefPtr<Session> mSession;
};
@@ -368,44 +405,94 @@ public:
: mRecorder(aRecorder)
, mTimeSlice(aTimeSlice)
, mStopIssued(false)
, mIsStartEventFired(false)
, mIsRegisterProfiler(false)
, mNeedSessionEndTask(true)
{
MOZ_ASSERT(NS_IsMainThread());
+ MOZ_COUNT_CTOR(MediaRecorder::Session);
uint32_t maxMem = Preferences::GetUint("media.recorder.max_memory",
MAX_ALLOW_MEMORY_BUFFER);
mEncodedBufferCache = new EncodedBufferCache(maxMem);
mLastBlobTimeStamp = TimeStamp::Now();
}
+ void PrincipalChanged(MediaStreamTrack* aTrack) override
+ {
+ NS_ASSERTION(mMediaStreamTracks.Contains(aTrack),
+ "Principal changed for unrecorded track");
+ if (!MediaStreamTracksPrincipalSubsumes()) {
+ DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+ }
+ }
+
+ void NotifyTrackAdded(const RefPtr<MediaStreamTrack>& aTrack) override
+ {
+ LOG(LogLevel::Warning, ("Session.NotifyTrackAdded %p Raising error due to track set change", this));
+ DoSessionEndTask(NS_ERROR_ABORT);
+ }
+
+ void NotifyTrackRemoved(const RefPtr<MediaStreamTrack>& aTrack) override
+ {
+ RefPtr<MediaInputPort> foundInputPort;
+ for (RefPtr<MediaInputPort> inputPort : mInputPorts) {
+ if (aTrack->IsForwardedThrough(inputPort)) {
+ foundInputPort = inputPort;
+ break;
+ }
+ }
+
+ if (foundInputPort) {
+ // A recorded track was removed or ended. End it in the recording.
+ // Don't raise an error.
+ foundInputPort->Destroy();
+ DebugOnly<bool> removed = mInputPorts.RemoveElement(foundInputPort);
+ MOZ_ASSERT(removed);
+ return;
+ }
+
+ LOG(LogLevel::Warning, ("Session.NotifyTrackRemoved %p Raising error due to track set change", this));
+ DoSessionEndTask(NS_ERROR_ABORT);
+ }
+
void Start()
{
LOG(LogLevel::Debug, ("Session.Start %p", this));
MOZ_ASSERT(NS_IsMainThread());
// Create a Track Union Stream
MediaStreamGraph* gm = mRecorder->GetSourceMediaStream()->Graph();
mTrackUnionStream = gm->CreateTrackUnionStream(nullptr);
MOZ_ASSERT(mTrackUnionStream, "CreateTrackUnionStream failed");
mTrackUnionStream->SetAutofinish(true);
- // Bind this Track Union Stream with Source Media.
- mInputPort = mTrackUnionStream->AllocateInputPort(mRecorder->GetSourceMediaStream());
-
DOMMediaStream* domStream = mRecorder->Stream();
if (domStream) {
- // Get the track type hint from DOM media stream.
+ // Get the available tracks from the DOMMediaStream.
+ // The callback will report back tracks that we have to connect to
+ // mTrackUnionStream and listen to principal changes on.
TracksAvailableCallback* tracksAvailableCallback = new TracksAvailableCallback(this);
domStream->OnTracksAvailable(tracksAvailableCallback);
} else {
+ // Check that we may access the audio node's content.
+ if (!AudioNodePrincipalSubsumes()) {
+ LOG(LogLevel::Warning, ("Session.Start AudioNode principal check failed"));
+ DoSessionEndTask(NS_ERROR_DOM_SECURITY_ERR);
+ return;
+ }
+ // Bind this Track Union Stream with Source Media.
+ RefPtr<MediaInputPort> inputPort =
+ mTrackUnionStream->AllocateInputPort(mRecorder->GetSourceMediaStream());
+ mInputPorts.AppendElement(inputPort.forget());
+ MOZ_ASSERT(mInputPorts[mInputPorts.Length()-1]);
+
// Web Audio node has only audio.
InitEncoder(ContainerWriter::CREATE_AUDIO_TRACK);
}
}
void Stop()
{
LOG(LogLevel::Debug, ("Session.Stop %p", this));
@@ -477,16 +564,17 @@ public:
return (mEncoder ? mEncoder->SizeOfExcludingThis(aMallocSizeOf) : 0);
}
private:
// Only DestroyRunnable is allowed to delete Session object.
virtual ~Session()
{
+ MOZ_COUNT_DTOR(MediaRecorder::Session);
LOG(LogLevel::Debug, ("Session.~Session (%p)", this));
CleanupStreams();
}
// Pull encoded media data from MediaEncoder and put into EncodedBufferCache.
// Destroy this session object in the end of this function.
// If the bool aForceFlush is true, we will force to dispatch a
// PushBlobRunnable to main thread.
void Extract(bool aForceFlush)
@@ -540,16 +628,68 @@ private:
if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
} else {
mLastBlobTimeStamp = TimeStamp::Now();
}
}
}
+ void MediaStreamReady(DOMMediaStream& aStream) {
+ mMediaStream = &aStream;
+ aStream.RegisterTrackListener(this);
+ }
+
+ void ConnectMediaStreamTrack(MediaStreamTrack& aTrack)
+ {
+ mMediaStreamTracks.AppendElement(&aTrack);
+ aTrack.AddPrincipalChangeObserver(this);
+ RefPtr<MediaInputPort> inputPort =
+ aTrack.ForwardTrackContentsTo(mTrackUnionStream);
+ MOZ_ASSERT(inputPort);
+ mInputPorts.AppendElement(inputPort.forget());
+ MOZ_ASSERT(mInputPorts[mInputPorts.Length()-1]);
+ }
+
+ bool PrincipalSubsumes(nsIPrincipal* aPrincipal)
+ {
+ if (!mRecorder->GetOwner())
+ return false;
+ nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc();
+ if (!doc) {
+ return false;
+ }
+ if (!aPrincipal) {
+ return false;
+ }
+ bool subsumes;
+ if (NS_FAILED(doc->NodePrincipal()->Subsumes(aPrincipal, &subsumes))) {
+ return false;
+ }
+ return subsumes;
+ }
+
+ bool MediaStreamTracksPrincipalSubsumes()
+ {
+ MOZ_ASSERT(mRecorder->mDOMStream);
+ nsCOMPtr<nsIPrincipal> principal = nullptr;
+ for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) {
+ nsContentUtils::CombineResourcePrincipals(&principal, track->GetPrincipal());
+ }
+ return PrincipalSubsumes(principal);
+ }
+
+ bool AudioNodePrincipalSubsumes()
+ {
+ MOZ_ASSERT(mRecorder->mAudioNode != nullptr);
+ nsIDocument* doc = mRecorder->mAudioNode->GetOwner()->GetExtantDoc();
+ nsCOMPtr<nsIPrincipal> principal = doc ? doc->NodePrincipal() : nullptr;
+ return PrincipalSubsumes(principal);
+ }
+
bool CheckPermission(const char* aType)
{
nsCOMPtr<nsIDocument> doc = mRecorder->GetOwner()->GetExtantDoc();
if (!doc) {
return false;
}
uint16_t appStatus = nsIPrincipal::APP_STATUS_NOT_INSTALLED;
@@ -657,35 +797,49 @@ private:
nsCOMPtr<nsIRunnable> runnable =
NS_NewRunnableMethodWithArg<nsresult>(mRecorder,
&MediaRecorder::NotifyError, rv);
NS_DispatchToMainThread(runnable);
}
if (NS_FAILED(NS_DispatchToMainThread(new EncoderErrorNotifierRunnable(this)))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread EncoderErrorNotifierRunnable failed");
}
- if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
- MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
+ if (rv != NS_ERROR_DOM_SECURITY_ERR) {
+ // Don't push a blob if there was a security error.
+ if (NS_FAILED(NS_DispatchToMainThread(new PushBlobRunnable(this)))) {
+ MOZ_ASSERT(false, "NS_DispatchToMainThread PushBlobRunnable failed");
+ }
}
if (NS_FAILED(NS_DispatchToMainThread(new DestroyRunnable(this)))) {
MOZ_ASSERT(false, "NS_DispatchToMainThread DestroyRunnable failed");
}
mNeedSessionEndTask = false;
}
void CleanupStreams()
{
- if (mInputPort.get()) {
- mInputPort->Destroy();
- mInputPort = nullptr;
+ for (RefPtr<MediaInputPort>& inputPort : mInputPorts) {
+ MOZ_ASSERT(inputPort);
+ inputPort->Destroy();
}
+ mInputPorts.Clear();
if (mTrackUnionStream.get()) {
mTrackUnionStream->Destroy();
mTrackUnionStream = nullptr;
}
+
+ if (mMediaStream) {
+ mMediaStream->UnregisterTrackListener(this);
+ mMediaStream = nullptr;
+ }
+
+ for (RefPtr<MediaStreamTrack>& track : mMediaStreamTracks) {
+ track->RemovePrincipalChangeObserver(this);
+ }
+ mMediaStreamTracks.Clear();
}
NS_IMETHODIMP Observe(nsISupports *aSubject, const char *aTopic, const char16_t *aData) override
{
MOZ_ASSERT(NS_IsMainThread());
LOG(LogLevel::Debug, ("Session.Observe XPCOM_SHUTDOWN %p", this));
if (strcmp(aTopic, NS_XPCOM_SHUTDOWN_OBSERVER_ID) == 0) {
// Force stop Session to terminate Read Thread.
@@ -714,17 +868,24 @@ private:
private:
// Hold reference to MediaRecoder that ensure MediaRecorder is alive
// if there is an active session. Access ONLY on main thread.
RefPtr<MediaRecorder> mRecorder;
// Receive track data from source and dispatch to Encoder.
// Pause/ Resume controller.
RefPtr<ProcessedMediaStream> mTrackUnionStream;
- RefPtr<MediaInputPort> mInputPort;
+ nsTArray<RefPtr<MediaInputPort>> mInputPorts;
+
+ // Stream currently recorded.
+ RefPtr<DOMMediaStream> mMediaStream;
+
+ // Tracks currently recorded. This should be a subset of mMediaStream's track
+ // set.
+ nsTArray<RefPtr<MediaStreamTrack>> mMediaStreamTracks;
// Runnable thread for read data from MediaEncode.
nsCOMPtr<nsIThread> mReadThread;
// MediaEncoder pipeline.
RefPtr<MediaEncoder> mEncoder;
// A buffer to cache encoded meda data.
nsAutoPtr<EncodedBufferCache> mEncodedBufferCache;
// Current session mimeType
@@ -850,27 +1011,16 @@ MediaRecorder::Start(const Optional<int3
return;
}
if (GetSourceMediaStream()->IsFinished() || GetSourceMediaStream()->IsDestroyed()) {
aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
return;
}
- // Check if source media stream is valid. See bug 919051.
- if (mDOMStream && !mDOMStream->GetPrincipal()) {
- aResult.Throw(NS_ERROR_DOM_INVALID_STATE_ERR);
- return;
- }
-
- if (!CheckPrincipal()) {
- aResult.Throw(NS_ERROR_DOM_SECURITY_ERR);
- return;
- }
-
int32_t timeSlice = 0;
if (aTimeSlice.WasPassed()) {
if (aTimeSlice.Value() < 0) {
aResult.Throw(NS_ERROR_INVALID_ARG);
return;
}
timeSlice = aTimeSlice.Value();
@@ -1028,21 +1178,17 @@ MediaRecorder::SetOptions(const MediaRec
mVideoBitsPerSecond = mBitsPerSecond;
}
}
nsresult
MediaRecorder::CreateAndDispatchBlobEvent(already_AddRefed<nsIDOMBlob>&& aBlob)
{
MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
- if (!CheckPrincipal()) {
- // Media is not same-origin, don't allow the data out.
- RefPtr<nsIDOMBlob> blob = aBlob;
- return NS_ERROR_DOM_SECURITY_ERR;
- }
+
BlobEventInit init;
init.mBubbles = false;
init.mCancelable = false;
nsCOMPtr<nsIDOMBlob> blob = aBlob;
init.mData = static_cast<Blob*>(blob.get());
RefPtr<BlobEvent> event =
@@ -1105,39 +1251,16 @@ MediaRecorder::NotifyError(nsresult aRv)
rv = DispatchDOMEvent(nullptr, event, nullptr, nullptr);
if (NS_FAILED(rv)) {
NS_ERROR("Failed to dispatch the error event!!!");
return;
}
return;
}
-bool MediaRecorder::CheckPrincipal()
-{
- MOZ_ASSERT(NS_IsMainThread(), "Not running on main thread");
- if (!mDOMStream && !mAudioNode) {
- return false;
- }
- if (!GetOwner())
- return false;
- nsCOMPtr<nsIDocument> doc = GetOwner()->GetExtantDoc();
- if (!doc) {
- return false;
- }
- nsIPrincipal* srcPrincipal = GetSourcePrincipal();
- if (!srcPrincipal) {
- return false;
- }
- bool subsumes;
- if (NS_FAILED(doc->NodePrincipal()->Subsumes(srcPrincipal, &subsumes))) {
- return false;
- }
- return subsumes;
-}
-
void
MediaRecorder::RemoveSession(Session* aSession)
{
LOG(LogLevel::Debug, ("MediaRecorder.RemoveSession (%p)", aSession));
mSessions.RemoveElement(aSession);
}
void
@@ -1163,27 +1286,16 @@ MediaRecorder::GetSourceMediaStream()
{
if (mDOMStream != nullptr) {
return mDOMStream->GetPlaybackStream();
}
MOZ_ASSERT(mAudioNode != nullptr);
return mPipeStream ? mPipeStream.get() : mAudioNode->GetStream();
}
-nsIPrincipal*
-MediaRecorder::GetSourcePrincipal()
-{
- if (mDOMStream != nullptr) {
- return mDOMStream->GetPrincipal();
- }
- MOZ_ASSERT(mAudioNode != nullptr);
- nsIDocument* doc = mAudioNode->GetOwner()->GetExtantDoc();
- return doc ? doc->NodePrincipal() : nullptr;
-}
-
size_t
MediaRecorder::SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const
{
size_t amount = 42;
for (size_t i = 0; i < mSessions.Length(); ++i) {
amount += mSessions[i]->SizeOfExcludingThis(aMallocSizeOf);
}
return amount;