Skip to content

Commit 199dad9

Browse files
committed
Reorganized Fairplay setup
1 parent 1c2e0c3 commit 199dad9

File tree

1 file changed

+138
-121
lines changed

1 file changed

+138
-121
lines changed

packages/playback-core/src/index.ts

Lines changed: 138 additions & 121 deletions
Original file line numberDiff line numberDiff line change
@@ -859,132 +859,124 @@ export const setupNativeFairplayDRM = (
859859
props: Partial<Pick<MuxMediaPropsInternal, 'playbackId' | 'tokens' | 'playbackToken' | 'customDomain' | 'drmTypeCb'>>,
860860
mediaEl: HTMLMediaElement
861861
) => {
862-
const onFpEncrypted = async (event: MediaEncryptedEvent) => {
863-
try {
864-
const initDataType = event.initDataType;
865-
if (initDataType !== 'skd') {
866-
console.error(`Received unexpected initialization data type "${initDataType}"`);
867-
return;
868-
}
869-
870-
if (!mediaEl.mediaKeys) {
871-
const access = await navigator
872-
.requestMediaKeySystemAccess('com.apple.fps', [
873-
{
874-
initDataTypes: [initDataType],
875-
videoCapabilities: [{ contentType: 'application/vnd.apple.mpegurl', robustness: '' }],
876-
distinctiveIdentifier: 'not-allowed',
877-
persistentState: 'not-allowed',
878-
sessionTypes: ['temporary'],
879-
},
880-
])
881-
.then((value) => {
882-
props.drmTypeCb?.(DRMType.FAIRPLAY);
883-
return value;
884-
})
885-
.catch(() => {
886-
const message = i18n(
887-
'Cannot play DRM-protected content with current security configuration on this browser. Try playing in another browser.'
888-
);
889-
// Should we flag this as a business exception?
890-
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
891-
mediaError.errorCategory = MuxErrorCategory.DRM;
892-
mediaError.muxCode = MuxErrorCode.ENCRYPTED_UNSUPPORTED_KEY_SYSTEM;
893-
saveAndDispatchError(mediaEl, mediaError);
894-
});
862+
const setupMediaKeys = async (
863+
props: Partial<
864+
Pick<MuxMediaPropsInternal, 'playbackId' | 'tokens' | 'playbackToken' | 'customDomain' | 'drmTypeCb'>
865+
>,
866+
mediaEl: HTMLMediaElement,
867+
initDataType: string
868+
) => {
869+
const access = await navigator
870+
.requestMediaKeySystemAccess('com.apple.fps', [
871+
{
872+
initDataTypes: [initDataType],
873+
videoCapabilities: [{ contentType: 'application/vnd.apple.mpegurl', robustness: '' }],
874+
distinctiveIdentifier: 'not-allowed',
875+
persistentState: 'not-allowed',
876+
sessionTypes: ['temporary'],
877+
},
878+
])
879+
.then((value) => {
880+
props.drmTypeCb?.(DRMType.FAIRPLAY);
881+
return value;
882+
})
883+
.catch(() => {
884+
const message = i18n(
885+
'Cannot play DRM-protected content with current security configuration on this browser. Try playing in another browser.'
886+
);
887+
// Should we flag this as a business exception?
888+
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
889+
mediaError.errorCategory = MuxErrorCategory.DRM;
890+
mediaError.muxCode = MuxErrorCode.ENCRYPTED_UNSUPPORTED_KEY_SYSTEM;
891+
saveAndDispatchError(mediaEl, mediaError);
892+
});
895893

896-
if (!access) return;
894+
if (!access) return;
897895

898-
const keys = await access.createMediaKeys();
896+
const keys = await access.createMediaKeys();
899897

900-
try {
901-
const fairPlayAppCert = await getAppCertificate(toAppCertURL(props, 'fairplay')).catch((errOrResp) => {
902-
if (errOrResp instanceof Response) {
903-
const mediaError = getErrorFromResponse(errOrResp, MuxErrorCategory.DRM, props);
904-
console.error('mediaError', mediaError?.message, mediaError?.context);
905-
if (mediaError) {
906-
return Promise.reject(mediaError);
907-
}
908-
// NOTE: This should never happen. Adding for exhaustiveness (CJP).
909-
return Promise.reject(new Error('Unexpected error in app cert request'));
910-
}
911-
return Promise.reject(errOrResp);
912-
});
913-
await keys.setServerCertificate(fairPlayAppCert).catch(() => {
914-
const message = i18n(
915-
'Your server certificate failed when attempting to set it. This may be an issue with a no longer valid certificate.'
916-
);
917-
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
918-
mediaError.errorCategory = MuxErrorCategory.DRM;
919-
mediaError.muxCode = MuxErrorCode.ENCRYPTED_UPDATE_SERVER_CERT_FAILED;
898+
try {
899+
const fairPlayAppCert = await getAppCertificate(toAppCertURL(props, 'fairplay')).catch((errOrResp) => {
900+
if (errOrResp instanceof Response) {
901+
const mediaError = getErrorFromResponse(errOrResp, MuxErrorCategory.DRM, props);
902+
console.error('mediaError', mediaError?.message, mediaError?.context);
903+
if (mediaError) {
920904
return Promise.reject(mediaError);
921-
});
922-
// @ts-ignore
923-
} catch (error: Error | MediaError) {
924-
saveAndDispatchError(mediaEl, error);
925-
return;
905+
}
906+
// NOTE: This should never happen. Adding for exhaustiveness (CJP).
907+
return Promise.reject(new Error('Unexpected error in app cert request'));
926908
}
927-
await mediaEl.setMediaKeys(keys);
928-
}
929-
930-
const initData = event.initData;
931-
if (initData == null) {
932-
console.error(`Could not start encrypted playback due to missing initData in ${event.type} event`);
933-
return;
934-
}
909+
return Promise.reject(errOrResp);
910+
});
911+
await keys.setServerCertificate(fairPlayAppCert).catch(() => {
912+
const message = i18n(
913+
'Your server certificate failed when attempting to set it. This may be an issue with a no longer valid certificate.'
914+
);
915+
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
916+
mediaError.errorCategory = MuxErrorCategory.DRM;
917+
mediaError.muxCode = MuxErrorCode.ENCRYPTED_UPDATE_SERVER_CERT_FAILED;
918+
return Promise.reject(mediaError);
919+
});
920+
// @ts-ignore
921+
} catch (error: Error | MediaError) {
922+
saveAndDispatchError(mediaEl, error);
923+
return;
924+
}
925+
await mediaEl.setMediaKeys(keys);
926+
};
935927

936-
const session = (mediaEl.mediaKeys as MediaKeys).createSession();
937-
session.addEventListener('keystatuseschange', () => {
938-
// recheck key statuses
939-
// NOTE: As an improvement, we could also add checks for a status of 'expired' and
940-
// attempt to renew the license here (CJP)
941-
session.keyStatuses.forEach((mediaKeyStatus) => {
942-
let mediaError;
943-
if (mediaKeyStatus === 'internal-error') {
944-
const message = i18n(
945-
'The DRM Content Decryption Module system had an internal failure. Try reloading the page, upading your browser, or playing in another browser.'
946-
);
947-
mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
948-
mediaError.errorCategory = MuxErrorCategory.DRM;
949-
mediaError.muxCode = MuxErrorCode.ENCRYPTED_CDM_ERROR;
950-
} else if (mediaKeyStatus === 'output-restricted' || mediaKeyStatus === 'output-downscaled') {
951-
const message = i18n(
952-
'DRM playback is being attempted in an environment that is not sufficiently secure. User may see black screen.'
953-
);
954-
// NOTE: When encountered, this is a non-fatal error (though it's certainly interruptive of standard playback experience). (CJP)
955-
mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, false);
956-
mediaError.errorCategory = MuxErrorCategory.DRM;
957-
mediaError.muxCode = MuxErrorCode.ENCRYPTED_OUTPUT_RESTRICTED;
958-
}
928+
const updateMediaKeyStatus = (mediaEl: HTMLMediaElement, mediaKeyStatus: MediaKeyStatus) => {
929+
let mediaError;
930+
if (mediaKeyStatus === 'internal-error') {
931+
const message = i18n(
932+
'The DRM Content Decryption Module system had an internal failure. Try reloading the page, upading your browser, or playing in another browser.'
933+
);
934+
mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
935+
mediaError.errorCategory = MuxErrorCategory.DRM;
936+
mediaError.muxCode = MuxErrorCode.ENCRYPTED_CDM_ERROR;
937+
} else if (mediaKeyStatus === 'output-restricted' || mediaKeyStatus === 'output-downscaled') {
938+
const message = i18n(
939+
'DRM playback is being attempted in an environment that is not sufficiently secure. User may see black screen.'
940+
);
941+
// NOTE: When encountered, this is a non-fatal error (though it's certainly interruptive of standard playback experience). (CJP)
942+
mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, false);
943+
mediaError.errorCategory = MuxErrorCategory.DRM;
944+
mediaError.muxCode = MuxErrorCode.ENCRYPTED_OUTPUT_RESTRICTED;
945+
}
959946

960-
if (mediaError) {
961-
saveAndDispatchError(mediaEl, mediaError);
962-
}
963-
});
964-
});
965-
const message = await Promise.all([
966-
session.generateRequest(initDataType, initData).catch(() => {
967-
// eslint-disable-next-line no-shadow
968-
const message = i18n(
969-
'Failed to generate a DRM license request. This may be an issue with the player or your protected content.'
970-
);
971-
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
972-
mediaError.errorCategory = MuxErrorCategory.DRM;
973-
mediaError.muxCode = MuxErrorCode.ENCRYPTED_GENERATE_REQUEST_FAILED;
974-
saveAndDispatchError(mediaEl, mediaError);
975-
}),
976-
new Promise<MediaKeyMessageEvent['message']>((resolve) => {
977-
session.addEventListener(
978-
'message',
979-
(messageEvent) => {
980-
resolve(messageEvent.message);
981-
},
982-
{ once: true }
983-
);
984-
}),
985-
]).then(([, messageEventMsg]) => messageEventMsg);
947+
if (mediaError) {
948+
saveAndDispatchError(mediaEl, mediaError);
949+
}
950+
};
986951

987-
const response = await getLicenseKey(message, toLicenseKeyURL(props, 'fairplay')).catch((errOrResp) => {
952+
const setupMediaKeySession = async (
953+
props: Partial<
954+
Pick<MuxMediaPropsInternal, 'playbackId' | 'tokens' | 'playbackToken' | 'customDomain' | 'drmTypeCb'>
955+
>,
956+
mediaEl: HTMLMediaElement,
957+
initDataType: string,
958+
initData: ArrayBuffer
959+
) => {
960+
const session = (mediaEl.mediaKeys as MediaKeys).createSession();
961+
session.addEventListener('keystatuseschange', () => {
962+
// recheck key statuses
963+
// NOTE: As an improvement, we could also add checks for a status of 'expired' and
964+
// attempt to renew the license here (CJP)
965+
session.keyStatuses.forEach((keyStatus) => updateMediaKeyStatus(mediaEl, keyStatus));
966+
});
967+
session.generateRequest(initDataType, initData).catch((e) => {
968+
console.error('Failed to generate license request', e);
969+
const message = i18n(
970+
'Failed to generate a DRM license request. This may be an issue with the player or your protected content.'
971+
);
972+
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
973+
mediaError.errorCategory = MuxErrorCategory.DRM;
974+
mediaError.muxCode = MuxErrorCode.ENCRYPTED_GENERATE_REQUEST_FAILED;
975+
saveAndDispatchError(mediaEl, mediaError);
976+
});
977+
session.addEventListener('message', async (event) => {
978+
const spc = event.message;
979+
const ckc = await getLicenseKey(spc, toLicenseKeyURL(props, 'fairplay')).catch((errOrResp) => {
988980
if (errOrResp instanceof Response) {
989981
const mediaError = getErrorFromResponse(errOrResp, MuxErrorCategory.DRM, props);
990982
console.error('mediaError', mediaError?.message, mediaError?.context);
@@ -996,16 +988,41 @@ export const setupNativeFairplayDRM = (
996988
}
997989
return Promise.reject(errOrResp);
998990
});
999-
await session.update(response).catch(() => {
1000-
// eslint-disable-next-line no-shadow
991+
992+
// This is the same call whether we are local or AirPlay.
993+
// Safari will forward CKC to Apple TV automatically.
994+
await session.update(ckc).catch(() => {
1001995
const message = i18n(
1002996
'Failed to update DRM license. This may be an issue with the player or your protected content.'
1003997
);
1004998
const mediaError = new MediaError(message, MediaError.MEDIA_ERR_ENCRYPTED, true);
1005999
mediaError.errorCategory = MuxErrorCategory.DRM;
10061000
mediaError.muxCode = MuxErrorCode.ENCRYPTED_UPDATE_LICENSE_FAILED;
1007-
return Promise.reject(mediaError);
1001+
1002+
saveAndDispatchError(mediaEl, mediaError);
10081003
});
1004+
});
1005+
};
1006+
1007+
const onFpEncrypted = async (event: MediaEncryptedEvent) => {
1008+
try {
1009+
const initDataType = event.initDataType;
1010+
if (initDataType !== 'skd') {
1011+
console.error(`Received unexpected initialization data type "${initDataType}"`);
1012+
return;
1013+
}
1014+
1015+
if (!mediaEl.mediaKeys) {
1016+
await setupMediaKeys(props, mediaEl, initDataType);
1017+
}
1018+
1019+
const initData = event.initData;
1020+
if (initData == null) {
1021+
console.error(`Could not start encrypted playback due to missing initData in ${event.type} event`);
1022+
return;
1023+
}
1024+
1025+
await setupMediaKeySession(props, mediaEl, initDataType, initData);
10091026
// @ts-ignore
10101027
} catch (error: Error | MediaError) {
10111028
saveAndDispatchError(mediaEl, error);

0 commit comments

Comments
 (0)