Source code

Revision control

Other Tools

1
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2
/* This Source Code Form is subject to the terms of the Mozilla Public
3
* License, v. 2.0. If a copy of the MPL was not distributed with this
4
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5
6
#include "ChromiumCDMParent.h"
7
8
#include "ChromiumCDMCallback.h"
9
#include "ChromiumCDMCallbackProxy.h"
10
#include "ChromiumCDMProxy.h"
11
#include "content_decryption_module.h"
12
#include "GMPContentChild.h"
13
#include "GMPContentParent.h"
14
#include "GMPLog.h"
15
#include "GMPService.h"
16
#include "GMPUtils.h"
17
#include "mozilla/dom/MediaKeyMessageEventBinding.h"
18
#include "mozilla/gmp/GMPTypes.h"
19
#include "mozilla/StaticPrefs.h"
20
#include "mozilla/Unused.h"
21
#include "AnnexB.h"
22
#include "H264.h"
23
24
#define NS_DispatchToMainThread(...) CompileError_UseAbstractMainThreadInstead
25
26
namespace mozilla {
27
namespace gmp {
28
29
using namespace eme;
30
31
ChromiumCDMParent::ChromiumCDMParent(GMPContentParent* aContentParent,
32
uint32_t aPluginId)
33
: mPluginId(aPluginId),
34
mContentParent(aContentParent),
35
mVideoShmemLimit(StaticPrefs::MediaEmeChromiumApiVideoShmems()) {
36
GMP_LOG(
37
"ChromiumCDMParent::ChromiumCDMParent(this=%p, contentParent=%p, id=%u)",
38
this, aContentParent, aPluginId);
39
}
40
41
RefPtr<ChromiumCDMParent::InitPromise> ChromiumCDMParent::Init(
42
ChromiumCDMCallback* aCDMCallback, bool aAllowDistinctiveIdentifier,
43
bool aAllowPersistentState, nsIEventTarget* aMainThread) {
44
GMP_LOG(
45
"ChromiumCDMParent::Init(this=%p) shutdown=%s abormalShutdown=%s "
46
"actorDestroyed=%s",
47
this, mIsShutdown ? "true" : "false",
48
mAbnormalShutdown ? "true" : "false", mActorDestroyed ? "true" : "false");
49
if (!aCDMCallback || !aMainThread) {
50
GMP_LOG(
51
"ChromiumCDMParent::Init(this=%p) failed "
52
"nullCallback=%s nullMainThread=%s",
53
this, !aCDMCallback ? "true" : "false",
54
!aMainThread ? "true" : "false");
55
56
return ChromiumCDMParent::InitPromise::CreateAndReject(
57
MediaResult(NS_ERROR_FAILURE,
58
nsPrintfCString("ChromiumCDMParent::Init() failed "
59
"nullCallback=%s nullMainThread=%s",
60
!aCDMCallback ? "true" : "false",
61
!aMainThread ? "true" : "false")),
62
__func__);
63
}
64
65
RefPtr<ChromiumCDMParent::InitPromise> promise =
66
mInitPromise.Ensure(__func__);
67
RefPtr<ChromiumCDMParent> self = this;
68
SendInit(aAllowDistinctiveIdentifier, aAllowPersistentState)
69
->Then(
70
AbstractThread::GetCurrent(), __func__,
71
[self, aCDMCallback](bool aSuccess) {
72
if (!aSuccess) {
73
GMP_LOG(
74
"ChromiumCDMParent::Init() failed with callback from "
75
"child indicating CDM failed init");
76
self->mInitPromise.RejectIfExists(
77
MediaResult(NS_ERROR_FAILURE,
78
"ChromiumCDMParent::Init() failed with callback "
79
"from child indicating CDM failed init"),
80
__func__);
81
return;
82
}
83
GMP_LOG(
84
"ChromiumCDMParent::Init() succeeded with callback from child");
85
self->mCDMCallback = aCDMCallback;
86
self->mInitPromise.ResolveIfExists(true /* unused */, __func__);
87
},
88
[self](ResponseRejectReason&& aReason) {
89
RefPtr<gmp::GeckoMediaPluginService> service =
90
gmp::GeckoMediaPluginService::GetGeckoMediaPluginService();
91
bool xpcomWillShutdown =
92
service && service->XPCOMWillShutdownReceived();
93
GMP_LOG(
94
"ChromiumCDMParent::Init(this=%p) failed "
95
"shutdown=%s cdmCrash=%s actorDestroyed=%s "
96
"browserShutdown=%s promiseRejectReason=%d",
97
self.get(), self->mIsShutdown ? "true" : "false",
98
self->mAbnormalShutdown ? "true" : "false",
99
self->mActorDestroyed ? "true" : "false",
100
xpcomWillShutdown ? "true" : "false",
101
static_cast<int>(aReason));
102
self->mInitPromise.RejectIfExists(
103
MediaResult(
104
NS_ERROR_FAILURE,
105
nsPrintfCString("ChromiumCDMParent::Init() failed "
106
"shutdown=%s cdmCrash=%s actorDestroyed=%s "
107
"browserShutdown=%s promiseRejectReason=%d",
108
self->mIsShutdown ? "true" : "false",
109
self->mAbnormalShutdown ? "true" : "false",
110
self->mActorDestroyed ? "true" : "false",
111
xpcomWillShutdown ? "true" : "false",
112
static_cast<int>(aReason))),
113
__func__);
114
});
115
return promise;
116
}
117
118
void ChromiumCDMParent::CreateSession(uint32_t aCreateSessionToken,
119
uint32_t aSessionType,
120
uint32_t aInitDataType,
121
uint32_t aPromiseId,
122
const nsTArray<uint8_t>& aInitData) {
123
GMP_LOG("ChromiumCDMParent::CreateSession(this=%p)", this);
124
if (mIsShutdown) {
125
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
126
NS_LITERAL_CSTRING("CDM is shutdown."));
127
return;
128
}
129
if (!SendCreateSessionAndGenerateRequest(aPromiseId, aSessionType,
130
aInitDataType, aInitData)) {
131
RejectPromise(
132
aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
133
NS_LITERAL_CSTRING("Failed to send generateRequest to CDM process."));
134
return;
135
}
136
mPromiseToCreateSessionToken.Put(aPromiseId, aCreateSessionToken);
137
}
138
139
void ChromiumCDMParent::LoadSession(uint32_t aPromiseId, uint32_t aSessionType,
140
nsString aSessionId) {
141
GMP_LOG("ChromiumCDMParent::LoadSession(this=%p, pid=%u, type=%u, sid=%s)",
142
this, aPromiseId, aSessionType,
143
NS_ConvertUTF16toUTF8(aSessionId).get());
144
if (mIsShutdown) {
145
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
146
NS_LITERAL_CSTRING("CDM is shutdown."));
147
return;
148
}
149
if (!SendLoadSession(aPromiseId, aSessionType,
150
NS_ConvertUTF16toUTF8(aSessionId))) {
151
RejectPromise(
152
aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
153
NS_LITERAL_CSTRING("Failed to send loadSession to CDM process."));
154
return;
155
}
156
}
157
158
void ChromiumCDMParent::SetServerCertificate(uint32_t aPromiseId,
159
const nsTArray<uint8_t>& aCert) {
160
GMP_LOG("ChromiumCDMParent::SetServerCertificate(this=%p)", this);
161
if (mIsShutdown) {
162
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
163
NS_LITERAL_CSTRING("CDM is shutdown."));
164
return;
165
}
166
if (!SendSetServerCertificate(aPromiseId, aCert)) {
167
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
168
NS_LITERAL_CSTRING(
169
"Failed to send setServerCertificate to CDM process"));
170
}
171
}
172
173
void ChromiumCDMParent::UpdateSession(const nsCString& aSessionId,
174
uint32_t aPromiseId,
175
const nsTArray<uint8_t>& aResponse) {
176
GMP_LOG("ChromiumCDMParent::UpdateSession(this=%p)", this);
177
if (mIsShutdown) {
178
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
179
NS_LITERAL_CSTRING("CDM is shutdown."));
180
return;
181
}
182
if (!SendUpdateSession(aPromiseId, aSessionId, aResponse)) {
183
RejectPromise(
184
aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
185
NS_LITERAL_CSTRING("Failed to send updateSession to CDM process"));
186
}
187
}
188
189
void ChromiumCDMParent::CloseSession(const nsCString& aSessionId,
190
uint32_t aPromiseId) {
191
GMP_LOG("ChromiumCDMParent::CloseSession(this=%p)", this);
192
if (mIsShutdown) {
193
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
194
NS_LITERAL_CSTRING("CDM is shutdown."));
195
return;
196
}
197
if (!SendCloseSession(aPromiseId, aSessionId)) {
198
RejectPromise(
199
aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
200
NS_LITERAL_CSTRING("Failed to send closeSession to CDM process"));
201
}
202
}
203
204
void ChromiumCDMParent::RemoveSession(const nsCString& aSessionId,
205
uint32_t aPromiseId) {
206
GMP_LOG("ChromiumCDMParent::RemoveSession(this=%p)", this);
207
if (mIsShutdown) {
208
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
209
NS_LITERAL_CSTRING("CDM is shutdown."));
210
return;
211
}
212
if (!SendRemoveSession(aPromiseId, aSessionId)) {
213
RejectPromise(
214
aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
215
NS_LITERAL_CSTRING("Failed to send removeSession to CDM process"));
216
}
217
}
218
219
void ChromiumCDMParent::GetStatusForPolicy(uint32_t aPromiseId,
220
const nsCString& aMinHdcpVersion) {
221
GMP_LOG("ChromiumCDMParent::GetStatusForPolicy(this=%p)", this);
222
if (mIsShutdown) {
223
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
224
NS_LITERAL_CSTRING("CDM is shutdown."));
225
return;
226
}
227
if (!SendGetStatusForPolicy(aPromiseId, aMinHdcpVersion)) {
228
RejectPromise(
229
aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
230
NS_LITERAL_CSTRING("Failed to send getStatusForPolicy to CDM process"));
231
}
232
}
233
234
bool ChromiumCDMParent::InitCDMInputBuffer(gmp::CDMInputBuffer& aBuffer,
235
MediaRawData* aSample) {
236
const CryptoSample& crypto = aSample->mCrypto;
237
if (crypto.mEncryptedSizes.Length() != crypto.mPlainSizes.Length()) {
238
GMP_LOG("InitCDMInputBuffer clear/cipher subsamples don't match");
239
return false;
240
}
241
242
Shmem shmem;
243
if (!AllocShmem(aSample->Size(), Shmem::SharedMemory::TYPE_BASIC, &shmem)) {
244
return false;
245
}
246
memcpy(shmem.get<uint8_t>(), aSample->Data(), aSample->Size());
247
GMPEncryptionScheme encryptionScheme =
248
GMPEncryptionScheme::kGMPEncryptionNone;
249
switch (crypto.mCryptoScheme) {
250
case CryptoScheme::None:
251
break; // Default to none
252
case CryptoScheme::Cenc:
253
encryptionScheme = GMPEncryptionScheme::kGMPEncryptionCenc;
254
break;
255
case CryptoScheme::Cbcs:
256
encryptionScheme = GMPEncryptionScheme::kGMPEncryptionCbcs;
257
break;
258
default:
259
GMP_LOG(
260
"InitCDMInputBuffer got unexpected encryption scheme with "
261
"value of %" PRIu8 ". Treating as no encryption.",
262
static_cast<uint8_t>(crypto.mCryptoScheme));
263
MOZ_ASSERT_UNREACHABLE("Should not have unrecognized encryption type");
264
break;
265
}
266
267
const nsTArray<uint8_t>& iv =
268
encryptionScheme != GMPEncryptionScheme::kGMPEncryptionCbcs
269
? crypto.mIV
270
: crypto.mConstantIV;
271
aBuffer = gmp::CDMInputBuffer(
272
std::move(shmem), crypto.mKeyId, iv, aSample->mTime.ToMicroseconds(),
273
aSample->mDuration.ToMicroseconds(), crypto.mPlainSizes,
274
crypto.mEncryptedSizes, crypto.mCryptByteBlock, crypto.mSkipByteBlock,
275
encryptionScheme);
276
MOZ_ASSERT(
277
aBuffer.mEncryptionScheme() == GMPEncryptionScheme::kGMPEncryptionNone ||
278
aBuffer.mEncryptionScheme() ==
279
GMPEncryptionScheme::kGMPEncryptionCenc ||
280
aBuffer.mEncryptionScheme() ==
281
GMPEncryptionScheme::kGMPEncryptionCbcs,
282
"aBuffer should use no encryption, cenc, or cbcs, other kinds are not "
283
"yet supported");
284
return true;
285
}
286
287
bool ChromiumCDMParent::SendBufferToCDM(uint32_t aSizeInBytes) {
288
GMP_LOG("ChromiumCDMParent::SendBufferToCDM() size=%" PRIu32, aSizeInBytes);
289
Shmem shmem;
290
if (!AllocShmem(aSizeInBytes, Shmem::SharedMemory::TYPE_BASIC, &shmem)) {
291
return false;
292
}
293
if (!SendGiveBuffer(std::move(shmem))) {
294
DeallocShmem(shmem);
295
return false;
296
}
297
return true;
298
}
299
300
RefPtr<DecryptPromise> ChromiumCDMParent::Decrypt(MediaRawData* aSample) {
301
if (mIsShutdown) {
302
return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
303
__func__);
304
}
305
CDMInputBuffer buffer;
306
if (!InitCDMInputBuffer(buffer, aSample)) {
307
return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
308
__func__);
309
}
310
// Send a buffer to the CDM to store the output. The CDM will either fill
311
// it with the decrypted sample and return it, or deallocate it on failure.
312
if (!SendBufferToCDM(aSample->Size())) {
313
DeallocShmem(buffer.mData());
314
return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
315
__func__);
316
}
317
318
RefPtr<DecryptJob> job = new DecryptJob(aSample);
319
if (!SendDecrypt(job->mId, buffer)) {
320
GMP_LOG(
321
"ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message",
322
this);
323
DeallocShmem(buffer.mData());
324
return DecryptPromise::CreateAndReject(DecryptResult(GenericErr, aSample),
325
__func__);
326
}
327
RefPtr<DecryptPromise> promise = job->Ensure();
328
mDecrypts.AppendElement(job);
329
return promise;
330
}
331
332
ipc::IPCResult ChromiumCDMParent::Recv__delete__() {
333
MOZ_ASSERT(mIsShutdown);
334
GMP_LOG("ChromiumCDMParent::Recv__delete__(this=%p)", this);
335
if (mContentParent) {
336
mContentParent->ChromiumCDMDestroyed(this);
337
mContentParent = nullptr;
338
}
339
return IPC_OK();
340
}
341
342
ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus(
343
const uint32_t& aPromiseId, const uint32_t& aKeyStatus) {
344
GMP_LOG(
345
"ChromiumCDMParent::RecvOnResolvePromiseWithKeyStatus(this=%p, pid=%u, "
346
"keystatus=%u)",
347
this, aPromiseId, aKeyStatus);
348
if (!mCDMCallback || mIsShutdown) {
349
return IPC_OK();
350
}
351
352
mCDMCallback->ResolvePromiseWithKeyStatus(aPromiseId, aKeyStatus);
353
354
return IPC_OK();
355
}
356
357
ipc::IPCResult ChromiumCDMParent::RecvOnResolveNewSessionPromise(
358
const uint32_t& aPromiseId, const nsCString& aSessionId) {
359
GMP_LOG(
360
"ChromiumCDMParent::RecvOnResolveNewSessionPromise(this=%p, pid=%u, "
361
"sid=%s)",
362
this, aPromiseId, aSessionId.get());
363
if (!mCDMCallback || mIsShutdown) {
364
return IPC_OK();
365
}
366
367
Maybe<uint32_t> token = mPromiseToCreateSessionToken.GetAndRemove(aPromiseId);
368
if (token.isNothing()) {
369
RejectPromise(aPromiseId, NS_ERROR_DOM_INVALID_STATE_ERR,
370
NS_LITERAL_CSTRING("Lost session token for new session."));
371
return IPC_OK();
372
}
373
374
mCDMCallback->SetSessionId(token.value(), aSessionId);
375
376
ResolvePromise(aPromiseId);
377
378
return IPC_OK();
379
}
380
381
ipc::IPCResult ChromiumCDMParent::RecvResolveLoadSessionPromise(
382
const uint32_t& aPromiseId, const bool& aSuccessful) {
383
GMP_LOG(
384
"ChromiumCDMParent::RecvResolveLoadSessionPromise(this=%p, pid=%u, "
385
"successful=%d)",
386
this, aPromiseId, aSuccessful);
387
if (!mCDMCallback || mIsShutdown) {
388
return IPC_OK();
389
}
390
391
mCDMCallback->ResolveLoadSessionPromise(aPromiseId, aSuccessful);
392
393
return IPC_OK();
394
}
395
396
void ChromiumCDMParent::ResolvePromise(uint32_t aPromiseId) {
397
GMP_LOG("ChromiumCDMParent::ResolvePromise(this=%p, pid=%u)", this,
398
aPromiseId);
399
400
// Note: The MediaKeys rejects all pending DOM promises when it
401
// initiates shutdown.
402
if (!mCDMCallback || mIsShutdown) {
403
return;
404
}
405
406
mCDMCallback->ResolvePromise(aPromiseId);
407
}
408
409
ipc::IPCResult ChromiumCDMParent::RecvOnResolvePromise(
410
const uint32_t& aPromiseId) {
411
ResolvePromise(aPromiseId);
412
return IPC_OK();
413
}
414
415
void ChromiumCDMParent::RejectPromise(uint32_t aPromiseId, nsresult aException,
416
const nsCString& aErrorMessage) {
417
GMP_LOG("ChromiumCDMParent::RejectPromise(this=%p, pid=%u)", this,
418
aPromiseId);
419
// Note: The MediaKeys rejects all pending DOM promises when it
420
// initiates shutdown.
421
if (!mCDMCallback || mIsShutdown) {
422
return;
423
}
424
425
mCDMCallback->RejectPromise(aPromiseId, aException, aErrorMessage);
426
}
427
428
static nsresult ToNsresult(uint32_t aException) {
429
switch (static_cast<cdm::Exception>(aException)) {
430
case cdm::Exception::kExceptionNotSupportedError:
431
return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
432
case cdm::Exception::kExceptionInvalidStateError:
433
return NS_ERROR_DOM_INVALID_STATE_ERR;
434
case cdm::Exception::kExceptionTypeError:
435
return NS_ERROR_DOM_TYPE_ERR;
436
case cdm::Exception::kExceptionQuotaExceededError:
437
return NS_ERROR_DOM_QUOTA_EXCEEDED_ERR;
438
};
439
MOZ_ASSERT_UNREACHABLE("Invalid cdm::Exception enum value.");
440
return NS_ERROR_DOM_TIMEOUT_ERR; // Note: Unique placeholder.
441
}
442
443
ipc::IPCResult ChromiumCDMParent::RecvOnRejectPromise(
444
const uint32_t& aPromiseId, const uint32_t& aException,
445
const uint32_t& aSystemCode, const nsCString& aErrorMessage) {
446
RejectPromise(aPromiseId, ToNsresult(aException), aErrorMessage);
447
return IPC_OK();
448
}
449
450
ipc::IPCResult ChromiumCDMParent::RecvOnSessionMessage(
451
const nsCString& aSessionId, const uint32_t& aMessageType,
452
nsTArray<uint8_t>&& aMessage) {
453
GMP_LOG("ChromiumCDMParent::RecvOnSessionMessage(this=%p, sid=%s)", this,
454
aSessionId.get());
455
if (!mCDMCallback || mIsShutdown) {
456
return IPC_OK();
457
}
458
459
mCDMCallback->SessionMessage(aSessionId, aMessageType, std::move(aMessage));
460
return IPC_OK();
461
}
462
463
ipc::IPCResult ChromiumCDMParent::RecvOnSessionKeysChange(
464
const nsCString& aSessionId, nsTArray<CDMKeyInformation>&& aKeysInfo) {
465
GMP_LOG("ChromiumCDMParent::RecvOnSessionKeysChange(this=%p)", this);
466
if (!mCDMCallback || mIsShutdown) {
467
return IPC_OK();
468
}
469
470
mCDMCallback->SessionKeysChange(aSessionId, std::move(aKeysInfo));
471
return IPC_OK();
472
}
473
474
ipc::IPCResult ChromiumCDMParent::RecvOnExpirationChange(
475
const nsCString& aSessionId, const double& aSecondsSinceEpoch) {
476
GMP_LOG("ChromiumCDMParent::RecvOnExpirationChange(this=%p) time=%lf", this,
477
aSecondsSinceEpoch);
478
if (!mCDMCallback || mIsShutdown) {
479
return IPC_OK();
480
}
481
482
mCDMCallback->ExpirationChange(aSessionId, aSecondsSinceEpoch);
483
return IPC_OK();
484
}
485
486
ipc::IPCResult ChromiumCDMParent::RecvOnSessionClosed(
487
const nsCString& aSessionId) {
488
GMP_LOG("ChromiumCDMParent::RecvOnSessionClosed(this=%p)", this);
489
if (!mCDMCallback || mIsShutdown) {
490
return IPC_OK();
491
}
492
493
mCDMCallback->SessionClosed(aSessionId);
494
return IPC_OK();
495
}
496
497
DecryptStatus ToDecryptStatus(uint32_t aStatus) {
498
switch (static_cast<cdm::Status>(aStatus)) {
499
case cdm::kSuccess:
500
return DecryptStatus::Ok;
501
case cdm::kNoKey:
502
return DecryptStatus::NoKeyErr;
503
default:
504
return DecryptStatus::GenericErr;
505
}
506
}
507
508
ipc::IPCResult ChromiumCDMParent::RecvDecryptFailed(const uint32_t& aId,
509
const uint32_t& aStatus) {
510
GMP_LOG("ChromiumCDMParent::RecvDecryptFailed(this=%p, id=%u, status=%u)",
511
this, aId, aStatus);
512
513
if (mIsShutdown) {
514
MOZ_ASSERT(mDecrypts.IsEmpty());
515
return IPC_OK();
516
}
517
518
for (size_t i = 0; i < mDecrypts.Length(); i++) {
519
if (mDecrypts[i]->mId == aId) {
520
mDecrypts[i]->PostResult(ToDecryptStatus(aStatus));
521
mDecrypts.RemoveElementAt(i);
522
break;
523
}
524
}
525
return IPC_OK();
526
}
527
528
ipc::IPCResult ChromiumCDMParent::RecvDecrypted(const uint32_t& aId,
529
const uint32_t& aStatus,
530
ipc::Shmem&& aShmem) {
531
GMP_LOG("ChromiumCDMParent::RecvDecrypted(this=%p, id=%u, status=%u)", this,
532
aId, aStatus);
533
534
// We must deallocate the shmem once we've copied the result out of it
535
// in PostResult below.
536
auto autoDeallocateShmem = MakeScopeExit([&, this] { DeallocShmem(aShmem); });
537
538
if (mIsShutdown) {
539
MOZ_ASSERT(mDecrypts.IsEmpty());
540
return IPC_OK();
541
}
542
for (size_t i = 0; i < mDecrypts.Length(); i++) {
543
if (mDecrypts[i]->mId == aId) {
544
mDecrypts[i]->PostResult(ToDecryptStatus(aStatus),
545
MakeSpan<const uint8_t>(aShmem.get<uint8_t>(),
546
aShmem.Size<uint8_t>()));
547
mDecrypts.RemoveElementAt(i);
548
break;
549
}
550
}
551
return IPC_OK();
552
}
553
554
ipc::IPCResult ChromiumCDMParent::RecvIncreaseShmemPoolSize() {
555
GMP_LOG("%s(this=%p) limit=%" PRIu32 " active=%" PRIu32, __func__, this,
556
mVideoShmemLimit, mVideoShmemsActive);
557
558
// Put an upper limit on the number of shmems we tolerate the CDM asking
559
// for, to prevent a memory blow-out. In practice, we expect the CDM to
560
// need less than 5, but some encodings require more.
561
// We'd expect CDMs to not have video frames larger than 720p-1080p
562
// (due to DRM robustness requirements), which is about 1.5MB-3MB per
563
// frame.
564
if (mVideoShmemLimit > 50) {
565
mDecodePromise.RejectIfExists(
566
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
567
RESULT_DETAIL("Failled to ensure CDM has enough shmems.")),
568
__func__);
569
Shutdown();
570
return IPC_OK();
571
}
572
mVideoShmemLimit++;
573
574
EnsureSufficientShmems(mVideoFrameBufferSize);
575
576
return IPC_OK();
577
}
578
579
bool ChromiumCDMParent::PurgeShmems() {
580
GMP_LOG(
581
"ChromiumCDMParent::PurgeShmems(this=%p) frame_size=%zu"
582
" limit=%" PRIu32 " active=%" PRIu32,
583
this, mVideoFrameBufferSize, mVideoShmemLimit, mVideoShmemsActive);
584
585
if (mVideoShmemsActive == 0) {
586
// We haven't allocated any shmems, nothing to do here.
587
return true;
588
}
589
if (!SendPurgeShmems()) {
590
return false;
591
}
592
mVideoShmemsActive = 0;
593
return true;
594
}
595
596
bool ChromiumCDMParent::EnsureSufficientShmems(size_t aVideoFrameSize) {
597
GMP_LOG(
598
"ChromiumCDMParent::EnsureSufficientShmems(this=%p) "
599
"size=%zu expected_size=%zu limit=%" PRIu32 " active=%" PRIu32,
600
this, aVideoFrameSize, mVideoFrameBufferSize, mVideoShmemLimit,
601
mVideoShmemsActive);
602
603
// The Chromium CDM API requires us to implement a synchronous
604
// interface to supply buffers to the CDM for it to write decrypted samples
605
// into. We want our buffers to be backed by shmems, in order to reduce
606
// the overhead of transferring decoded frames. However due to sandboxing
607
// restrictions, the CDM process cannot allocate shmems itself.
608
// We don't want to be doing synchronous IPC to request shmems from the
609
// content process, nor do we want to have to do intr IPC or make async
610
// IPC conform to the sync allocation interface. So instead we have the
611
// content process pre-allocate a set of shmems and give them to the CDM
612
// process in advance of them being needed.
613
//
614
// When the CDM needs to allocate a buffer for storing a decoded video
615
// frame, the CDM host gives it one of these shmems' buffers. When this
616
// is sent back to the content process, we upload it to a GPU surface,
617
// and send the shmem back to the CDM process so it can reuse it.
618
//
619
// Normally the CDM won't allocate more than one buffer at once, but
620
// we've seen cases where it allocates multiple buffers, returns one and
621
// holds onto the rest. So we need to ensure we have several extra
622
// shmems pre-allocated for the CDM. This threshold is set by the pref
623
// media.eme.chromium-api.video-shmems.
624
//
625
// We also have a failure recovery mechanism; if the CDM asks for more
626
// buffers than we have shmem's available, ChromiumCDMChild gives the
627
// CDM a non-shared memory buffer, and returns the frame to the parent
628
// in an nsTArray<uint8_t> instead of a shmem. The child then sends a
629
// message to the parent asking it to increase the number of shmems in
630
// the pool. Via this mechanism we should recover from incorrectly
631
// predicting how many shmems to pre-allocate.
632
//
633
// At decoder start up, we guess how big the shmems need to be based on
634
// the video frame dimensions. If we guess wrong, the CDM will follow
635
// the non-shmem path, and we'll re-create the shmems of the correct size.
636
// This meanns we can recover from guessing the shmem size wrong.
637
// We must re-take this path after every decoder de-init/re-init, as the
638
// frame sizes should change every time we switch video stream.
639
640
if (mVideoFrameBufferSize < aVideoFrameSize) {
641
if (!PurgeShmems()) {
642
return false;
643
}
644
mVideoFrameBufferSize = aVideoFrameSize;
645
}
646
647
while (mVideoShmemsActive < mVideoShmemLimit) {
648
if (!SendBufferToCDM(mVideoFrameBufferSize)) {
649
return false;
650
}
651
mVideoShmemsActive++;
652
}
653
654
return true;
655
}
656
657
ipc::IPCResult ChromiumCDMParent::RecvDecodedData(const CDMVideoFrame& aFrame,
658
nsTArray<uint8_t>&& aData) {
659
GMP_LOG("ChromiumCDMParent::RecvDecodedData(this=%p) time=%" PRId64, this,
660
aFrame.mTimestamp());
661
662
if (mIsShutdown || mDecodePromise.IsEmpty()) {
663
return IPC_OK();
664
}
665
666
if (!EnsureSufficientShmems(aData.Length())) {
667
mDecodePromise.RejectIfExists(
668
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
669
RESULT_DETAIL("Failled to ensure CDM has enough shmems.")),
670
__func__);
671
return IPC_OK();
672
}
673
674
RefPtr<VideoData> v = CreateVideoFrame(aFrame, aData);
675
if (!v) {
676
mDecodePromise.RejectIfExists(
677
MediaResult(NS_ERROR_OUT_OF_MEMORY,
678
RESULT_DETAIL("Can't create VideoData")),
679
__func__);
680
return IPC_OK();
681
}
682
683
ReorderAndReturnOutput(std::move(v));
684
685
return IPC_OK();
686
}
687
688
ipc::IPCResult ChromiumCDMParent::RecvDecodedShmem(const CDMVideoFrame& aFrame,
689
ipc::Shmem&& aShmem) {
690
GMP_LOG("ChromiumCDMParent::RecvDecodedShmem(this=%p) time=%" PRId64
691
" duration=%" PRId64,
692
this, aFrame.mTimestamp(), aFrame.mDuration());
693
694
// On failure we need to deallocate the shmem we're to return to the
695
// CDM. On success we return it to the CDM to be reused.
696
auto autoDeallocateShmem =
697
MakeScopeExit([&, this] { this->DeallocShmem(aShmem); });
698
699
if (mIsShutdown || mDecodePromise.IsEmpty()) {
700
return IPC_OK();
701
}
702
703
RefPtr<VideoData> v = CreateVideoFrame(
704
aFrame, MakeSpan<uint8_t>(aShmem.get<uint8_t>(), aShmem.Size<uint8_t>()));
705
if (!v) {
706
mDecodePromise.RejectIfExists(
707
MediaResult(NS_ERROR_OUT_OF_MEMORY,
708
RESULT_DETAIL("Can't create VideoData")),
709
__func__);
710
return IPC_OK();
711
}
712
713
// Return the shmem to the CDM so the shmem can be reused to send us
714
// another frame.
715
if (!SendGiveBuffer(std::move(aShmem))) {
716
mDecodePromise.RejectIfExists(
717
MediaResult(NS_ERROR_OUT_OF_MEMORY,
718
RESULT_DETAIL("Can't return shmem to CDM process")),
719
__func__);
720
return IPC_OK();
721
}
722
723
// Don't need to deallocate the shmem since the CDM process is responsible
724
// for it again.
725
autoDeallocateShmem.release();
726
727
ReorderAndReturnOutput(std::move(v));
728
729
return IPC_OK();
730
}
731
732
void ChromiumCDMParent::ReorderAndReturnOutput(RefPtr<VideoData>&& aFrame) {
733
if (mMaxRefFrames == 0) {
734
mDecodePromise.ResolveIfExists(
735
MediaDataDecoder::DecodedData({std::move(aFrame)}), __func__);
736
return;
737
}
738
mReorderQueue.Push(std::move(aFrame));
739
MediaDataDecoder::DecodedData results;
740
while (mReorderQueue.Length() > mMaxRefFrames) {
741
results.AppendElement(mReorderQueue.Pop());
742
}
743
mDecodePromise.Resolve(std::move(results), __func__);
744
}
745
746
already_AddRefed<VideoData> ChromiumCDMParent::CreateVideoFrame(
747
const CDMVideoFrame& aFrame, Span<uint8_t> aData) {
748
VideoData::YCbCrBuffer b;
749
MOZ_ASSERT(aData.Length() > 0);
750
751
b.mPlanes[0].mData = aData.Elements();
752
b.mPlanes[0].mWidth = aFrame.mImageWidth();
753
b.mPlanes[0].mHeight = aFrame.mImageHeight();
754
b.mPlanes[0].mStride = aFrame.mYPlane().mStride();
755
b.mPlanes[0].mOffset = aFrame.mYPlane().mPlaneOffset();
756
b.mPlanes[0].mSkip = 0;
757
758
b.mPlanes[1].mData = aData.Elements();
759
b.mPlanes[1].mWidth = (aFrame.mImageWidth() + 1) / 2;
760
b.mPlanes[1].mHeight = (aFrame.mImageHeight() + 1) / 2;
761
b.mPlanes[1].mStride = aFrame.mUPlane().mStride();
762
b.mPlanes[1].mOffset = aFrame.mUPlane().mPlaneOffset();
763
b.mPlanes[1].mSkip = 0;
764
765
b.mPlanes[2].mData = aData.Elements();
766
b.mPlanes[2].mWidth = (aFrame.mImageWidth() + 1) / 2;
767
b.mPlanes[2].mHeight = (aFrame.mImageHeight() + 1) / 2;
768
b.mPlanes[2].mStride = aFrame.mVPlane().mStride();
769
b.mPlanes[2].mOffset = aFrame.mVPlane().mPlaneOffset();
770
b.mPlanes[2].mSkip = 0;
771
772
gfx::IntRect pictureRegion(0, 0, aFrame.mImageWidth(), aFrame.mImageHeight());
773
RefPtr<VideoData> v = VideoData::CreateAndCopyData(
774
mVideoInfo, mImageContainer, mLastStreamOffset,
775
media::TimeUnit::FromMicroseconds(aFrame.mTimestamp()),
776
media::TimeUnit::FromMicroseconds(aFrame.mDuration()), b, false,
777
media::TimeUnit::FromMicroseconds(-1), pictureRegion);
778
779
return v.forget();
780
}
781
782
ipc::IPCResult ChromiumCDMParent::RecvDecodeFailed(const uint32_t& aStatus) {
783
if (mIsShutdown) {
784
MOZ_ASSERT(mDecodePromise.IsEmpty());
785
return IPC_OK();
786
}
787
788
if (aStatus == cdm::kNeedMoreData) {
789
mDecodePromise.ResolveIfExists(nsTArray<RefPtr<MediaData>>(), __func__);
790
return IPC_OK();
791
}
792
793
mDecodePromise.RejectIfExists(
794
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
795
RESULT_DETAIL("ChromiumCDMParent::RecvDecodeFailed")),
796
__func__);
797
return IPC_OK();
798
}
799
800
ipc::IPCResult ChromiumCDMParent::RecvShutdown() {
801
GMP_LOG("ChromiumCDMParent::RecvShutdown(this=%p)", this);
802
Shutdown();
803
return IPC_OK();
804
}
805
806
void ChromiumCDMParent::ActorDestroy(ActorDestroyReason aWhy) {
807
GMP_LOG("ChromiumCDMParent::ActorDestroy(this=%p, reason=%d)", this, aWhy);
808
MOZ_ASSERT(!mActorDestroyed);
809
mActorDestroyed = true;
810
// Shutdown() will clear mCDMCallback, so let's keep a reference for later
811
// use.
812
auto callback = mCDMCallback;
813
if (!mIsShutdown) {
814
// Plugin crash.
815
MOZ_ASSERT(aWhy == AbnormalShutdown);
816
Shutdown();
817
}
818
MOZ_ASSERT(mIsShutdown);
819
RefPtr<ChromiumCDMParent> kungFuDeathGrip(this);
820
if (mContentParent) {
821
mContentParent->ChromiumCDMDestroyed(this);
822
mContentParent = nullptr;
823
}
824
mAbnormalShutdown = (aWhy == AbnormalShutdown);
825
if (mAbnormalShutdown && callback) {
826
callback->Terminated();
827
}
828
MaybeDisconnect(mAbnormalShutdown);
829
}
830
831
RefPtr<MediaDataDecoder::InitPromise> ChromiumCDMParent::InitializeVideoDecoder(
832
const gmp::CDMVideoDecoderConfig& aConfig, const VideoInfo& aInfo,
833
RefPtr<layers::ImageContainer> aImageContainer) {
834
if (mIsShutdown) {
835
return MediaDataDecoder::InitPromise::CreateAndReject(
836
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
837
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
838
__func__);
839
}
840
841
// The Widevine CDM version 1.4.8.970 and above contain a video decoder that
842
// does not optimally allocate video frames; it requests buffers much larger
843
// than required. The exact formula the CDM uses to calculate their frame
844
// sizes isn't obvious, but they normally request around or slightly more
845
// than 1.5X the optimal amount. So pad the size of buffers we allocate so
846
// that we're likely to have buffers big enough to accomodate the CDM's weird
847
// frame size calculation.
848
const size_t bufferSize =
849
1.7 * I420FrameBufferSizePadded(aInfo.mImage.width, aInfo.mImage.height);
850
if (bufferSize <= 0) {
851
return MediaDataDecoder::InitPromise::CreateAndReject(
852
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
853
RESULT_DETAIL("Video frame buffer size is invalid.")),
854
__func__);
855
}
856
857
if (!EnsureSufficientShmems(bufferSize)) {
858
return MediaDataDecoder::InitPromise::CreateAndReject(
859
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
860
RESULT_DETAIL("Failed to init shmems for video decoder")),
861
__func__);
862
}
863
864
if (!SendInitializeVideoDecoder(aConfig)) {
865
return MediaDataDecoder::InitPromise::CreateAndReject(
866
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
867
RESULT_DETAIL("Failed to send init video decoder to CDM")),
868
__func__);
869
}
870
871
mMaxRefFrames = (aConfig.mCodec() == cdm::VideoCodec::kCodecH264)
872
? H264::HasSPS(aInfo.mExtraData)
873
? H264::ComputeMaxRefFrames(aInfo.mExtraData)
874
: 16
875
: 0;
876
877
mVideoDecoderInitialized = true;
878
mImageContainer = aImageContainer;
879
mVideoInfo = aInfo;
880
mVideoFrameBufferSize = bufferSize;
881
882
return mInitVideoDecoderPromise.Ensure(__func__);
883
}
884
885
ipc::IPCResult ChromiumCDMParent::RecvOnDecoderInitDone(
886
const uint32_t& aStatus) {
887
GMP_LOG("ChromiumCDMParent::RecvOnDecoderInitDone(this=%p, status=%u)", this,
888
aStatus);
889
if (mIsShutdown) {
890
MOZ_ASSERT(mInitVideoDecoderPromise.IsEmpty());
891
return IPC_OK();
892
}
893
if (aStatus == static_cast<uint32_t>(cdm::kSuccess)) {
894
mInitVideoDecoderPromise.ResolveIfExists(TrackInfo::kVideoTrack, __func__);
895
} else {
896
mVideoDecoderInitialized = false;
897
mInitVideoDecoderPromise.RejectIfExists(
898
MediaResult(
899
NS_ERROR_DOM_MEDIA_FATAL_ERR,
900
RESULT_DETAIL("CDM init decode failed with %" PRIu32, aStatus)),
901
__func__);
902
}
903
return IPC_OK();
904
}
905
906
RefPtr<MediaDataDecoder::DecodePromise>
907
ChromiumCDMParent::DecryptAndDecodeFrame(MediaRawData* aSample) {
908
if (mIsShutdown) {
909
return MediaDataDecoder::DecodePromise::CreateAndReject(
910
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
911
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
912
__func__);
913
}
914
915
GMP_LOG("ChromiumCDMParent::DecryptAndDecodeFrame t=%" PRId64,
916
aSample->mTime.ToMicroseconds());
917
918
CDMInputBuffer buffer;
919
920
if (!InitCDMInputBuffer(buffer, aSample)) {
921
return MediaDataDecoder::DecodePromise::CreateAndReject(
922
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR, "Failed to init CDM buffer."),
923
__func__);
924
}
925
926
mLastStreamOffset = aSample->mOffset;
927
928
if (!SendDecryptAndDecodeFrame(buffer)) {
929
GMP_LOG(
930
"ChromiumCDMParent::Decrypt(this=%p) failed to send decrypt message.",
931
this);
932
DeallocShmem(buffer.mData());
933
return MediaDataDecoder::DecodePromise::CreateAndReject(
934
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
935
"Failed to send decrypt to CDM process."),
936
__func__);
937
}
938
939
return mDecodePromise.Ensure(__func__);
940
}
941
942
RefPtr<MediaDataDecoder::FlushPromise> ChromiumCDMParent::FlushVideoDecoder() {
943
if (mIsShutdown) {
944
MOZ_ASSERT(mReorderQueue.IsEmpty());
945
return MediaDataDecoder::FlushPromise::CreateAndReject(
946
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
947
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
948
__func__);
949
}
950
951
mReorderQueue.Clear();
952
953
mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
954
if (!SendResetVideoDecoder()) {
955
return MediaDataDecoder::FlushPromise::CreateAndReject(
956
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
957
"Failed to send flush to CDM."),
958
__func__);
959
}
960
return mFlushDecoderPromise.Ensure(__func__);
961
}
962
963
ipc::IPCResult ChromiumCDMParent::RecvResetVideoDecoderComplete() {
964
MOZ_ASSERT(mReorderQueue.IsEmpty());
965
if (mIsShutdown) {
966
MOZ_ASSERT(mFlushDecoderPromise.IsEmpty());
967
return IPC_OK();
968
}
969
mFlushDecoderPromise.ResolveIfExists(true, __func__);
970
return IPC_OK();
971
}
972
973
RefPtr<MediaDataDecoder::DecodePromise> ChromiumCDMParent::Drain() {
974
MOZ_ASSERT(mDecodePromise.IsEmpty(), "Must wait for decoding to complete");
975
if (mIsShutdown) {
976
return MediaDataDecoder::DecodePromise::CreateAndReject(
977
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
978
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
979
__func__);
980
}
981
982
RefPtr<MediaDataDecoder::DecodePromise> p = mDecodePromise.Ensure(__func__);
983
if (!SendDrain()) {
984
mDecodePromise.Resolve(MediaDataDecoder::DecodedData(), __func__);
985
}
986
return p;
987
}
988
989
ipc::IPCResult ChromiumCDMParent::RecvDrainComplete() {
990
if (mIsShutdown) {
991
MOZ_ASSERT(mDecodePromise.IsEmpty());
992
return IPC_OK();
993
}
994
995
MediaDataDecoder::DecodedData samples;
996
while (!mReorderQueue.IsEmpty()) {
997
samples.AppendElement(mReorderQueue.Pop());
998
}
999
1000
mDecodePromise.ResolveIfExists(std::move(samples), __func__);
1001
return IPC_OK();
1002
}
1003
RefPtr<ShutdownPromise> ChromiumCDMParent::ShutdownVideoDecoder() {
1004
if (mIsShutdown || !mVideoDecoderInitialized) {
1005
return ShutdownPromise::CreateAndResolve(true, __func__);
1006
}
1007
mInitVideoDecoderPromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED,
1008
__func__);
1009
mDecodePromise.RejectIfExists(NS_ERROR_DOM_MEDIA_CANCELED, __func__);
1010
MOZ_ASSERT(mFlushDecoderPromise.IsEmpty());
1011
if (!SendDeinitializeVideoDecoder()) {
1012
return ShutdownPromise::CreateAndResolve(true, __func__);
1013
}
1014
mVideoDecoderInitialized = false;
1015
1016
GMP_LOG("ChromiumCDMParent::~ShutdownVideoDecoder(this=%p) ", this);
1017
1018
// The ChromiumCDMChild will purge its shmems, so if the decoder is
1019
// reinitialized the shmems need to be re-allocated, and they may need
1020
// to be a different size.
1021
mVideoShmemsActive = 0;
1022
mVideoFrameBufferSize = 0;
1023
return ShutdownPromise::CreateAndResolve(true, __func__);
1024
}
1025
1026
void ChromiumCDMParent::Shutdown() {
1027
GMP_LOG("ChromiumCDMParent::Shutdown(this=%p)", this);
1028
1029
if (mIsShutdown) {
1030
return;
1031
}
1032
mIsShutdown = true;
1033
1034
// If we're shutting down due to the plugin shutting down due to application
1035
// shutdown, we should tell the CDM proxy to also shutdown. Otherwise the
1036
// proxy will shutdown when the owning MediaKeys is destroyed during cycle
1037
// collection, and that will not shut down cleanly as the GMP thread will be
1038
// shutdown by then.
1039
if (mCDMCallback) {
1040
mCDMCallback->Shutdown();
1041
}
1042
1043
// We may be called from a task holding the last reference to the CDM
1044
// callback, so let's clear our local weak pointer to ensure it will not be
1045
// used afterward (including from an already-queued task, e.g.: ActorDestroy).
1046
mCDMCallback = nullptr;
1047
1048
mReorderQueue.Clear();
1049
1050
for (RefPtr<DecryptJob>& decrypt : mDecrypts) {
1051
decrypt->PostResult(eme::AbortedErr);
1052
}
1053
mDecrypts.Clear();
1054
1055
if (mVideoDecoderInitialized && !mActorDestroyed) {
1056
Unused << SendDeinitializeVideoDecoder();
1057
mVideoDecoderInitialized = false;
1058
}
1059
1060
// Note: MediaKeys rejects all outstanding promises when it initiates
1061
// shutdown.
1062
mPromiseToCreateSessionToken.Clear();
1063
1064
mInitPromise.RejectIfExists(
1065
MediaResult(NS_ERROR_DOM_ABORT_ERR,
1066
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
1067
__func__);
1068
1069
mInitVideoDecoderPromise.RejectIfExists(
1070
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
1071
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
1072
__func__);
1073
mDecodePromise.RejectIfExists(
1074
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
1075
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
1076
__func__);
1077
mFlushDecoderPromise.RejectIfExists(
1078
MediaResult(NS_ERROR_DOM_MEDIA_FATAL_ERR,
1079
RESULT_DETAIL("ChromiumCDMParent is shutdown")),
1080
__func__);
1081
1082
if (!mActorDestroyed) {
1083
Unused << SendDestroy();
1084
}
1085
}
1086
1087
} // namespace gmp
1088
} // namespace mozilla
1089
1090
#undef NS_DispatchToMainThread