1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6
7 #include "ChannelMediaDecoder.h"
8 #include "DecoderTraits.h"
9 #include "MediaDecoderStateMachine.h"
10 #include "MediaFormatReader.h"
11 #include "BaseMediaResource.h"
12 #include "MediaShutdownManager.h"
13
14 namespace mozilla {
15
16 extern LazyLogModule gMediaDecoderLog;
17 #define LOG(x, ...) \
18 DDMOZ_LOG(gMediaDecoderLog, LogLevel::Debug, x, ##__VA_ARGS__)
19
ResourceCallback(AbstractThread * aMainThread)20 ChannelMediaDecoder::ResourceCallback::ResourceCallback(
21 AbstractThread* aMainThread)
22 : mAbstractMainThread(aMainThread) {
23 MOZ_ASSERT(aMainThread);
24 DecoderDoctorLogger::LogConstructionAndBase(
25 "ChannelMediaDecoder::ResourceCallback", this,
26 static_cast<const MediaResourceCallback*>(this));
27 }
28
~ResourceCallback()29 ChannelMediaDecoder::ResourceCallback::~ResourceCallback() {
30 DecoderDoctorLogger::LogDestruction("ChannelMediaDecoder::ResourceCallback",
31 this);
32 }
33
Connect(ChannelMediaDecoder * aDecoder)34 void ChannelMediaDecoder::ResourceCallback::Connect(
35 ChannelMediaDecoder* aDecoder) {
36 MOZ_ASSERT(NS_IsMainThread());
37 mDecoder = aDecoder;
38 DecoderDoctorLogger::LinkParentAndChild(
39 "ChannelMediaDecoder::ResourceCallback", this, "decoder", mDecoder);
40 mTimer = NS_NewTimer(mAbstractMainThread->AsEventTarget());
41 }
42
Disconnect()43 void ChannelMediaDecoder::ResourceCallback::Disconnect() {
44 MOZ_ASSERT(NS_IsMainThread());
45 if (mDecoder) {
46 DecoderDoctorLogger::UnlinkParentAndChild(
47 "ChannelMediaDecoder::ResourceCallback", this, mDecoder);
48 mDecoder = nullptr;
49 mTimer->Cancel();
50 mTimer = nullptr;
51 }
52 }
53
AbstractMainThread() const54 AbstractThread* ChannelMediaDecoder::ResourceCallback::AbstractMainThread()
55 const {
56 return mAbstractMainThread;
57 }
58
GetMediaOwner() const59 MediaDecoderOwner* ChannelMediaDecoder::ResourceCallback::GetMediaOwner()
60 const {
61 MOZ_ASSERT(NS_IsMainThread());
62 return mDecoder ? mDecoder->GetOwner() : nullptr;
63 }
64
NotifyNetworkError(const MediaResult & aError)65 void ChannelMediaDecoder::ResourceCallback::NotifyNetworkError(
66 const MediaResult& aError) {
67 MOZ_ASSERT(NS_IsMainThread());
68 DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
69 "network_error", aError);
70 if (mDecoder) {
71 mDecoder->NetworkError(aError);
72 }
73 }
74
TimerCallback(nsITimer * aTimer,void * aClosure)75 /* static */ void ChannelMediaDecoder::ResourceCallback::TimerCallback(
76 nsITimer* aTimer, void* aClosure) {
77 MOZ_ASSERT(NS_IsMainThread());
78 ResourceCallback* thiz = static_cast<ResourceCallback*>(aClosure);
79 MOZ_ASSERT(thiz->mDecoder);
80 thiz->mDecoder->NotifyReaderDataArrived();
81 thiz->mTimerArmed = false;
82 }
83
NotifyDataArrived()84 void ChannelMediaDecoder::ResourceCallback::NotifyDataArrived() {
85 MOZ_ASSERT(NS_IsMainThread());
86 DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
87 "data_arrived", true);
88
89 if (!mDecoder) {
90 return;
91 }
92
93 mDecoder->DownloadProgressed();
94
95 if (mTimerArmed) {
96 return;
97 }
98 // In situations where these notifications come from stochastic network
99 // activity, we can save significant computation by throttling the
100 // calls to MediaDecoder::NotifyDataArrived() which will update the buffer
101 // ranges of the reader.
102 mTimerArmed = true;
103 mTimer->InitWithNamedFuncCallback(
104 TimerCallback, this, sDelay, nsITimer::TYPE_ONE_SHOT,
105 "ChannelMediaDecoder::ResourceCallback::TimerCallback");
106 }
107
NotifyDataEnded(nsresult aStatus)108 void ChannelMediaDecoder::ResourceCallback::NotifyDataEnded(nsresult aStatus) {
109 DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
110 "data_ended", aStatus);
111 MOZ_ASSERT(NS_IsMainThread());
112 if (mDecoder) {
113 mDecoder->NotifyDownloadEnded(aStatus);
114 }
115 }
116
NotifyPrincipalChanged()117 void ChannelMediaDecoder::ResourceCallback::NotifyPrincipalChanged() {
118 MOZ_ASSERT(NS_IsMainThread());
119 DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
120 "principal_changed", true);
121 if (mDecoder) {
122 mDecoder->NotifyPrincipalChanged();
123 }
124 }
125
NotifyPrincipalChanged()126 void ChannelMediaDecoder::NotifyPrincipalChanged() {
127 MOZ_ASSERT(NS_IsMainThread());
128 MediaDecoder::NotifyPrincipalChanged();
129 if (!mInitialChannelPrincipalKnown) {
130 // We'll receive one notification when the channel's initial principal
131 // is known, after all HTTP redirects have resolved. This isn't really a
132 // principal change, so return here to avoid the mSameOriginMedia check
133 // below.
134 mInitialChannelPrincipalKnown = true;
135 return;
136 }
137 if (!mSameOriginMedia &&
138 DecoderTraits::CrossOriginRedirectsProhibited(ContainerType())) {
139 // For some content types we block mid-flight channel redirects to cross
140 // origin destinations due to security constraints. See bug 1441153.
141 LOG("ChannnelMediaDecoder prohibited cross origin redirect blocked.");
142 NetworkError(MediaResult(NS_ERROR_DOM_BAD_URI,
143 "Prohibited cross origin redirect blocked"));
144 }
145 }
146
NotifySuspendedStatusChanged(bool aSuspendedByCache)147 void ChannelMediaDecoder::ResourceCallback::NotifySuspendedStatusChanged(
148 bool aSuspendedByCache) {
149 MOZ_ASSERT(NS_IsMainThread());
150 DDLOGEX2("ChannelMediaDecoder::ResourceCallback", this, DDLogCategory::Log,
151 "suspended_status_changed", aSuspendedByCache);
152 MediaDecoderOwner* owner = GetMediaOwner();
153 if (owner) {
154 AbstractThread::AutoEnter context(owner->AbstractMainThread());
155 owner->NotifySuspendedByCache(aSuspendedByCache);
156 }
157 }
158
ChannelMediaDecoder(MediaDecoderInit & aInit)159 ChannelMediaDecoder::ChannelMediaDecoder(MediaDecoderInit& aInit)
160 : MediaDecoder(aInit),
161 mResourceCallback(
162 new ResourceCallback(aInit.mOwner->AbstractMainThread())) {
163 mResourceCallback->Connect(this);
164 }
165
166 /* static */
Create(MediaDecoderInit & aInit,DecoderDoctorDiagnostics * aDiagnostics)167 already_AddRefed<ChannelMediaDecoder> ChannelMediaDecoder::Create(
168 MediaDecoderInit& aInit, DecoderDoctorDiagnostics* aDiagnostics) {
169 MOZ_ASSERT(NS_IsMainThread());
170 RefPtr<ChannelMediaDecoder> decoder;
171
172 const MediaContainerType& type = aInit.mContainerType;
173 if (DecoderTraits::IsSupportedType(type)) {
174 decoder = new ChannelMediaDecoder(aInit);
175 return decoder.forget();
176 }
177
178 if (DecoderTraits::IsHttpLiveStreamingType(type)) {
179 // We don't have an HLS decoder.
180 Telemetry::Accumulate(Telemetry::MEDIA_HLS_DECODER_SUCCESS, false);
181 }
182
183 return nullptr;
184 }
185
CanClone()186 bool ChannelMediaDecoder::CanClone() {
187 MOZ_ASSERT(NS_IsMainThread());
188 return mResource && mResource->CanClone();
189 }
190
Clone(MediaDecoderInit & aInit)191 already_AddRefed<ChannelMediaDecoder> ChannelMediaDecoder::Clone(
192 MediaDecoderInit& aInit) {
193 if (!mResource || !DecoderTraits::IsSupportedType(aInit.mContainerType)) {
194 return nullptr;
195 }
196 RefPtr<ChannelMediaDecoder> decoder = new ChannelMediaDecoder(aInit);
197 if (!decoder) {
198 return nullptr;
199 }
200 nsresult rv = decoder->Load(mResource);
201 if (NS_FAILED(rv)) {
202 decoder->Shutdown();
203 return nullptr;
204 }
205 return decoder.forget();
206 }
207
CreateStateMachine()208 MediaDecoderStateMachine* ChannelMediaDecoder::CreateStateMachine() {
209 MOZ_ASSERT(NS_IsMainThread());
210 MediaFormatReaderInit init;
211 init.mVideoFrameContainer = GetVideoFrameContainer();
212 init.mKnowsCompositor = GetCompositor();
213 init.mCrashHelper = GetOwner()->CreateGMPCrashHelper();
214 init.mFrameStats = mFrameStats;
215 init.mResource = mResource;
216 init.mMediaDecoderOwnerID = mOwner;
217 mReader = DecoderTraits::CreateReader(ContainerType(), init);
218 return new MediaDecoderStateMachine(this, mReader);
219 }
220
Shutdown()221 void ChannelMediaDecoder::Shutdown() {
222 mResourceCallback->Disconnect();
223 MediaDecoder::Shutdown();
224
225 // Force any outstanding seek and byterange requests to complete
226 // to prevent shutdown from deadlocking.
227 if (mResource) {
228 mResource->Close();
229 }
230 }
231
Load(nsIChannel * aChannel,bool aIsPrivateBrowsing,nsIStreamListener ** aStreamListener)232 nsresult ChannelMediaDecoder::Load(nsIChannel* aChannel,
233 bool aIsPrivateBrowsing,
234 nsIStreamListener** aStreamListener) {
235 MOZ_ASSERT(NS_IsMainThread());
236 MOZ_ASSERT(!mResource);
237 MOZ_ASSERT(aStreamListener);
238 AbstractThread::AutoEnter context(AbstractMainThread());
239
240 mResource = BaseMediaResource::Create(mResourceCallback, aChannel,
241 aIsPrivateBrowsing);
242 if (!mResource) {
243 return NS_ERROR_FAILURE;
244 }
245 DDLINKCHILD("resource", mResource.get());
246
247 nsresult rv = MediaShutdownManager::Instance().Register(this);
248 if (NS_WARN_IF(NS_FAILED(rv))) {
249 return rv;
250 }
251
252 rv = mResource->Open(aStreamListener);
253 NS_ENSURE_SUCCESS(rv, rv);
254
255 SetStateMachine(CreateStateMachine());
256 NS_ENSURE_TRUE(GetStateMachine(), NS_ERROR_FAILURE);
257
258 GetStateMachine()->DispatchIsLiveStream(mResource->IsLiveStream());
259
260 return InitializeStateMachine();
261 }
262
Load(BaseMediaResource * aOriginal)263 nsresult ChannelMediaDecoder::Load(BaseMediaResource* aOriginal) {
264 MOZ_ASSERT(NS_IsMainThread());
265 MOZ_ASSERT(!mResource);
266 AbstractThread::AutoEnter context(AbstractMainThread());
267
268 mResource = aOriginal->CloneData(mResourceCallback);
269 if (!mResource) {
270 return NS_ERROR_FAILURE;
271 }
272 DDLINKCHILD("resource", mResource.get());
273
274 nsresult rv = MediaShutdownManager::Instance().Register(this);
275 if (NS_WARN_IF(NS_FAILED(rv))) {
276 return rv;
277 }
278
279 SetStateMachine(CreateStateMachine());
280 NS_ENSURE_TRUE(GetStateMachine(), NS_ERROR_FAILURE);
281
282 GetStateMachine()->DispatchIsLiveStream(mResource->IsLiveStream());
283
284 return InitializeStateMachine();
285 }
286
NotifyDownloadEnded(nsresult aStatus)287 void ChannelMediaDecoder::NotifyDownloadEnded(nsresult aStatus) {
288 MOZ_ASSERT(NS_IsMainThread());
289 MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
290 AbstractThread::AutoEnter context(AbstractMainThread());
291
292 LOG("NotifyDownloadEnded, status=%" PRIx32, static_cast<uint32_t>(aStatus));
293
294 if (NS_SUCCEEDED(aStatus)) {
295 // Download ends successfully. This is a stream with a finite length.
296 GetStateMachine()->DispatchIsLiveStream(false);
297 }
298
299 MediaDecoderOwner* owner = GetOwner();
300 if (NS_SUCCEEDED(aStatus) || aStatus == NS_BASE_STREAM_CLOSED) {
301 nsCOMPtr<nsIRunnable> r =
302 NS_NewRunnableFunction("ChannelMediaDecoder::UpdatePlaybackRate", [
303 stats = mPlaybackStatistics,
304 res = RefPtr<BaseMediaResource>(mResource), duration = mDuration
305 ]() {
306 auto rate = ComputePlaybackRate(stats, res, duration);
307 UpdatePlaybackRate(rate, res);
308 });
309 nsresult rv = GetStateMachine()->OwnerThread()->Dispatch(r.forget());
310 MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
311 Unused << rv;
312 owner->DownloadSuspended();
313 // NotifySuspendedStatusChanged will tell the element that download
314 // has been suspended "by the cache", which is true since we never
315 // download anything. The element can then transition to HAVE_ENOUGH_DATA.
316 owner->NotifySuspendedByCache(true);
317 } else if (aStatus == NS_BINDING_ABORTED) {
318 // Download has been cancelled by user.
319 owner->LoadAborted();
320 } else {
321 NetworkError(MediaResult(aStatus, "Download aborted"));
322 }
323 }
324
CanPlayThroughImpl()325 bool ChannelMediaDecoder::CanPlayThroughImpl() {
326 MOZ_ASSERT(NS_IsMainThread());
327 return mCanPlayThrough;
328 }
329
OnPlaybackEvent(MediaPlaybackEvent && aEvent)330 void ChannelMediaDecoder::OnPlaybackEvent(MediaPlaybackEvent&& aEvent) {
331 MOZ_ASSERT(NS_IsMainThread());
332 switch (aEvent.mType) {
333 case MediaPlaybackEvent::PlaybackStarted:
334 mPlaybackPosition = aEvent.mData.as<int64_t>();
335 mPlaybackStatistics.Start();
336 break;
337 case MediaPlaybackEvent::PlaybackProgressed: {
338 int64_t newPos = aEvent.mData.as<int64_t>();
339 mPlaybackStatistics.AddBytes(newPos - mPlaybackPosition);
340 mPlaybackPosition = newPos;
341 break;
342 }
343 case MediaPlaybackEvent::PlaybackStopped: {
344 int64_t newPos = aEvent.mData.as<int64_t>();
345 mPlaybackStatistics.AddBytes(newPos - mPlaybackPosition);
346 mPlaybackPosition = newPos;
347 mPlaybackStatistics.Stop();
348 break;
349 }
350 default:
351 break;
352 }
353 MediaDecoder::OnPlaybackEvent(Move(aEvent));
354 }
355
DurationChanged()356 void ChannelMediaDecoder::DurationChanged() {
357 MOZ_ASSERT(NS_IsMainThread());
358 AbstractThread::AutoEnter context(AbstractMainThread());
359 MediaDecoder::DurationChanged();
360 // Duration has changed so we should recompute playback rate
361 nsCOMPtr<nsIRunnable> r =
362 NS_NewRunnableFunction("ChannelMediaDecoder::UpdatePlaybackRate", [
363 stats = mPlaybackStatistics, res = RefPtr<BaseMediaResource>(mResource),
364 duration = mDuration
365 ]() {
366 auto rate = ComputePlaybackRate(stats, res, duration);
367 UpdatePlaybackRate(rate, res);
368 });
369 nsresult rv = GetStateMachine()->OwnerThread()->Dispatch(r.forget());
370 MOZ_DIAGNOSTIC_ASSERT(NS_SUCCEEDED(rv));
371 Unused << rv;
372 }
373
DownloadProgressed()374 void ChannelMediaDecoder::DownloadProgressed() {
375 MOZ_ASSERT(NS_IsMainThread());
376 MOZ_DIAGNOSTIC_ASSERT(!IsShutdown());
377
378 GetOwner()->DownloadProgressed();
379
380 using StatsPromise = MozPromise<MediaStatistics, bool, true>;
381 InvokeAsync(GetStateMachine()->OwnerThread(), __func__,
382 [
383 playbackStats = mPlaybackStatistics,
384 res = RefPtr<BaseMediaResource>(mResource),
385 duration = mDuration, pos = mPlaybackPosition
386 ]() {
387 auto rate = ComputePlaybackRate(playbackStats, res, duration);
388 UpdatePlaybackRate(rate, res);
389 MediaStatistics stats = GetStatistics(rate, res, pos);
390 return StatsPromise::CreateAndResolve(stats, __func__);
391 })
392 ->Then(mAbstractMainThread, __func__,
393 [ =, self = RefPtr<ChannelMediaDecoder>(this) ](
394 MediaStatistics aStats) {
395 if (IsShutdown()) {
396 return;
397 }
398 mCanPlayThrough = aStats.CanPlayThrough();
399 GetStateMachine()->DispatchCanPlayThrough(mCanPlayThrough);
400 mResource->ThrottleReadahead(ShouldThrottleDownload(aStats));
401 // Update readyState since mCanPlayThrough might have changed.
402 GetOwner()->UpdateReadyState();
403 },
404 []() { MOZ_ASSERT_UNREACHABLE("Promise not resolved"); });
405 }
406
407 /* static */ ChannelMediaDecoder::PlaybackRateInfo
ComputePlaybackRate(const MediaChannelStatistics & aStats,BaseMediaResource * aResource,double aDuration)408 ChannelMediaDecoder::ComputePlaybackRate(const MediaChannelStatistics& aStats,
409 BaseMediaResource* aResource,
410 double aDuration) {
411 MOZ_ASSERT(!NS_IsMainThread());
412
413 int64_t length = aResource->GetLength();
414 if (mozilla::IsFinite<double>(aDuration) && aDuration > 0 && length >= 0) {
415 return {uint32_t(length / aDuration), true};
416 }
417
418 bool reliable = false;
419 uint32_t rate = aStats.GetRate(&reliable);
420 return {rate, reliable};
421 }
422
UpdatePlaybackRate(const PlaybackRateInfo & aInfo,BaseMediaResource * aResource)423 /* static */ void ChannelMediaDecoder::UpdatePlaybackRate(
424 const PlaybackRateInfo& aInfo, BaseMediaResource* aResource) {
425 MOZ_ASSERT(!NS_IsMainThread());
426
427 uint32_t rate = aInfo.mRate;
428
429 if (aInfo.mReliable) {
430 // Avoid passing a zero rate
431 rate = std::max(rate, 1u);
432 } else {
433 // Set a minimum rate of 10,000 bytes per second ... sometimes we just
434 // don't have good data
435 rate = std::max(rate, 10000u);
436 }
437
438 aResource->SetPlaybackRate(rate);
439 }
440
GetStatistics(const PlaybackRateInfo & aInfo,BaseMediaResource * aRes,int64_t aPlaybackPosition)441 /* static */ MediaStatistics ChannelMediaDecoder::GetStatistics(
442 const PlaybackRateInfo& aInfo, BaseMediaResource* aRes,
443 int64_t aPlaybackPosition) {
444 MOZ_ASSERT(!NS_IsMainThread());
445
446 MediaStatistics result;
447 result.mDownloadRate = aRes->GetDownloadRate(&result.mDownloadRateReliable);
448 result.mDownloadPosition = aRes->GetCachedDataEnd(aPlaybackPosition);
449 result.mTotalBytes = aRes->GetLength();
450 result.mPlaybackRate = aInfo.mRate;
451 result.mPlaybackRateReliable = aInfo.mReliable;
452 result.mPlaybackPosition = aPlaybackPosition;
453 return result;
454 }
455
ShouldThrottleDownload(const MediaStatistics & aStats)456 bool ChannelMediaDecoder::ShouldThrottleDownload(
457 const MediaStatistics& aStats) {
458 // We throttle the download if either the throttle override pref is set
459 // (so that we can always throttle in Firefox on mobile) or if the download
460 // is fast enough that there's no concern about playback being interrupted.
461 MOZ_ASSERT(NS_IsMainThread());
462 NS_ENSURE_TRUE(GetStateMachine(), false);
463
464 int64_t length = aStats.mTotalBytes;
465 if (length > 0 &&
466 length <= int64_t(MediaPrefs::MediaMemoryCacheMaxSize()) * 1024) {
467 // Don't throttle the download of small resources. This is to speed
468 // up seeking, as seeks into unbuffered ranges would require starting
469 // up a new HTTP transaction, which adds latency.
470 return false;
471 }
472
473 if (Preferences::GetBool("media.throttle-regardless-of-download-rate",
474 false)) {
475 return true;
476 }
477
478 if (!aStats.mDownloadRateReliable || !aStats.mPlaybackRateReliable) {
479 return false;
480 }
481 uint32_t factor =
482 std::max(2u, Preferences::GetUint("media.throttle-factor", 2));
483 return aStats.mDownloadRate > factor * aStats.mPlaybackRate;
484 }
485
AddSizeOfResources(ResourceSizes * aSizes)486 void ChannelMediaDecoder::AddSizeOfResources(ResourceSizes* aSizes) {
487 MOZ_ASSERT(NS_IsMainThread());
488 if (mResource) {
489 aSizes->mByteSize += mResource->SizeOfIncludingThis(aSizes->mMallocSizeOf);
490 }
491 }
492
GetCurrentPrincipal()493 already_AddRefed<nsIPrincipal> ChannelMediaDecoder::GetCurrentPrincipal() {
494 MOZ_ASSERT(NS_IsMainThread());
495 return mResource ? mResource->GetCurrentPrincipal() : nullptr;
496 }
497
IsTransportSeekable()498 bool ChannelMediaDecoder::IsTransportSeekable() {
499 MOZ_ASSERT(NS_IsMainThread());
500 return mResource->IsTransportSeekable();
501 }
502
SetLoadInBackground(bool aLoadInBackground)503 void ChannelMediaDecoder::SetLoadInBackground(bool aLoadInBackground) {
504 MOZ_ASSERT(NS_IsMainThread());
505 if (mResource) {
506 mResource->SetLoadInBackground(aLoadInBackground);
507 }
508 }
509
Suspend()510 void ChannelMediaDecoder::Suspend() {
511 MOZ_ASSERT(NS_IsMainThread());
512 if (mResource) {
513 mResource->Suspend(true);
514 }
515 }
516
Resume()517 void ChannelMediaDecoder::Resume() {
518 MOZ_ASSERT(NS_IsMainThread());
519 if (mResource) {
520 mResource->Resume();
521 }
522 }
523
MetadataLoaded(UniquePtr<MediaInfo> aInfo,UniquePtr<MetadataTags> aTags,MediaDecoderEventVisibility aEventVisibility)524 void ChannelMediaDecoder::MetadataLoaded(
525 UniquePtr<MediaInfo> aInfo, UniquePtr<MetadataTags> aTags,
526 MediaDecoderEventVisibility aEventVisibility) {
527 MediaDecoder::MetadataLoaded(Move(aInfo), Move(aTags), aEventVisibility);
528 // Set mode to PLAYBACK after reading metadata.
529 mResource->SetReadMode(MediaCacheStream::MODE_PLAYBACK);
530 }
531
GetDebugInfo()532 nsCString ChannelMediaDecoder::GetDebugInfo() {
533 auto&& str = MediaDecoder::GetDebugInfo();
534 if (mResource) {
535 AppendStringIfNotEmpty(str, mResource->GetDebugInfo());
536 }
537 return str;
538 }
539
540 } // namespace mozilla
541
542 // avoid redefined macro in unified build
543 #undef LOG
544