@@ -882,158 +882,184 @@ export const setupNativeFairplayDRM = (
882882 props : Partial < Pick < MuxMediaPropsInternal , 'playbackId' | 'tokens' | 'playbackToken' | 'customDomain' | 'drmTypeCb' > > ,
883883 mediaEl : HTMLMediaElement
884884) => {
885- const onFpEncrypted = async ( event : MediaEncryptedEvent ) => {
886- try {
887- const initDataType = event . initDataType ;
888- if ( initDataType !== 'skd' ) {
889- console . error ( `Received unexpected initialization data type "${ initDataType } "` ) ;
890- return ;
891- }
892-
893- if ( ! mediaEl . mediaKeys ) {
894- const access = await navigator
895- . requestMediaKeySystemAccess ( 'com.apple.fps' , [
896- {
897- initDataTypes : [ initDataType ] ,
898- videoCapabilities : [ { contentType : 'application/vnd.apple.mpegurl' , robustness : '' } ] ,
899- distinctiveIdentifier : 'not-allowed' ,
900- persistentState : 'not-allowed' ,
901- sessionTypes : [ 'temporary' ] ,
902- } ,
903- ] )
904- . then ( ( value ) => {
905- props . drmTypeCb ?.( DRMType . FAIRPLAY ) ;
906- return value ;
907- } )
908- . catch ( ( ) => {
909- const message = i18n (
910- 'Cannot play DRM-protected content with current security configuration on this browser. Try playing in another browser.'
911- ) ;
912- // Should we flag this as a business exception?
913- const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
914- mediaError . errorCategory = MuxErrorCategory . DRM ;
915- mediaError . muxCode = MuxErrorCode . ENCRYPTED_UNSUPPORTED_KEY_SYSTEM ;
916- saveAndDispatchError ( mediaEl , mediaError ) ;
917- } ) ;
918-
919- if ( ! access ) return ;
920-
921- const keys = await access . createMediaKeys ( ) ;
922-
923- try {
924- const fairPlayAppCert = await getAppCertificate ( toAppCertURL ( props , 'fairplay' ) ) . catch ( ( errOrResp ) => {
925- if ( errOrResp instanceof Response ) {
926- const mediaError = getErrorFromResponse ( errOrResp , MuxErrorCategory . DRM , props ) ;
927- console . error ( 'mediaError' , mediaError ?. message , mediaError ?. context ) ;
928- if ( mediaError ) {
929- return Promise . reject ( mediaError ) ;
930- }
931- // NOTE: This should never happen. Adding for exhaustiveness (CJP).
932- return Promise . reject ( new Error ( 'Unexpected error in app cert request' ) ) ;
933- }
934- return Promise . reject ( errOrResp ) ;
935- } ) ;
936- await keys . setServerCertificate ( fairPlayAppCert ) . catch ( ( ) => {
937- const message = i18n (
938- 'Your server certificate failed when attempting to set it. This may be an issue with a no longer valid certificate.'
939- ) ;
940- const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
941- mediaError . errorCategory = MuxErrorCategory . DRM ;
942- mediaError . muxCode = MuxErrorCode . ENCRYPTED_UPDATE_SERVER_CERT_FAILED ;
943- return Promise . reject ( mediaError ) ;
944- } ) ;
945- // @ts -ignore
946- } catch ( error : Error | MediaError ) {
947- saveAndDispatchError ( mediaEl , error ) ;
948- return ;
949- }
950- await mediaEl . setMediaKeys ( keys ) ;
951- }
952-
953- const initData = event . initData ;
954- if ( initData == null ) {
955- console . error ( `Could not start encrypted playback due to missing initData in ${ event . type } event` ) ;
956- return ;
957- }
885+ const setupMediaKeys = async ( initDataType : string ) => {
886+ const access = await navigator
887+ . requestMediaKeySystemAccess ( 'com.apple.fps' , [
888+ {
889+ initDataTypes : [ initDataType ] ,
890+ videoCapabilities : [ { contentType : 'application/vnd.apple.mpegurl' , robustness : '' } ] ,
891+ distinctiveIdentifier : 'not-allowed' ,
892+ persistentState : 'not-allowed' ,
893+ sessionTypes : [ 'temporary' ] ,
894+ } ,
895+ ] )
896+ . then ( ( value ) => {
897+ props . drmTypeCb ?.( DRMType . FAIRPLAY ) ;
898+ return value ;
899+ } )
900+ . catch ( ( ) => {
901+ const message = i18n (
902+ 'Cannot play DRM-protected content with current security configuration on this browser. Try playing in another browser.'
903+ ) ;
904+ // Should we flag this as a business exception?
905+ const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
906+ mediaError . errorCategory = MuxErrorCategory . DRM ;
907+ mediaError . muxCode = MuxErrorCode . ENCRYPTED_UNSUPPORTED_KEY_SYSTEM ;
908+ saveAndDispatchError ( mediaEl , mediaError ) ;
909+ } ) ;
958910
959- const session = ( mediaEl . mediaKeys as MediaKeys ) . createSession ( ) ;
960- session . addEventListener ( 'keystatuseschange' , ( ) => {
961- // recheck key statuses
962- // NOTE: As an improvement, we could also add checks for a status of 'expired' and
963- // attempt to renew the license here (CJP)
964- session . keyStatuses . forEach ( ( mediaKeyStatus ) => {
965- let mediaError ;
966- if ( mediaKeyStatus === 'internal-error' ) {
967- const message = i18n (
968- 'The DRM Content Decryption Module system had an internal failure. Try reloading the page, upading your browser, or playing in another browser.'
969- ) ;
970- mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
971- mediaError . errorCategory = MuxErrorCategory . DRM ;
972- mediaError . muxCode = MuxErrorCode . ENCRYPTED_CDM_ERROR ;
973- } else if ( mediaKeyStatus === 'output-restricted' || mediaKeyStatus === 'output-downscaled' ) {
974- const message = i18n (
975- 'DRM playback is being attempted in an environment that is not sufficiently secure. User may see black screen.'
976- ) ;
977- // NOTE: When encountered, this is a non-fatal error (though it's certainly interruptive of standard playback experience). (CJP)
978- mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , false ) ;
979- mediaError . errorCategory = MuxErrorCategory . DRM ;
980- mediaError . muxCode = MuxErrorCode . ENCRYPTED_OUTPUT_RESTRICTED ;
981- }
911+ if ( ! access ) return ;
982912
983- if ( mediaError ) {
984- saveAndDispatchError ( mediaEl , mediaError ) ;
985- }
986- } ) ;
987- } ) ;
988- const message = await Promise . all ( [
989- session . generateRequest ( initDataType , initData ) . catch ( ( ) => {
990- // eslint-disable-next-line no-shadow
991- const message = i18n (
992- 'Failed to generate a DRM license request. This may be an issue with the player or your protected content.'
993- ) ;
994- const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
995- mediaError . errorCategory = MuxErrorCategory . DRM ;
996- mediaError . muxCode = MuxErrorCode . ENCRYPTED_GENERATE_REQUEST_FAILED ;
997- saveAndDispatchError ( mediaEl , mediaError ) ;
998- } ) ,
999- new Promise < MediaKeyMessageEvent [ 'message' ] > ( ( resolve ) => {
1000- session . addEventListener (
1001- 'message' ,
1002- ( messageEvent ) => {
1003- resolve ( messageEvent . message ) ;
1004- } ,
1005- { once : true }
1006- ) ;
1007- } ) ,
1008- ] ) . then ( ( [ , messageEventMsg ] ) => messageEventMsg ) ;
913+ const keys = await access . createMediaKeys ( ) ;
1009914
1010- const response = await getLicenseKey ( message , toLicenseKeyURL ( props , 'fairplay' ) ) . catch ( ( errOrResp ) => {
915+ try {
916+ const fairPlayAppCert = await getAppCertificate ( toAppCertURL ( props , 'fairplay' ) ) . catch ( ( errOrResp ) => {
1011917 if ( errOrResp instanceof Response ) {
1012918 const mediaError = getErrorFromResponse ( errOrResp , MuxErrorCategory . DRM , props ) ;
1013919 console . error ( 'mediaError' , mediaError ?. message , mediaError ?. context ) ;
1014920 if ( mediaError ) {
1015921 return Promise . reject ( mediaError ) ;
1016922 }
1017923 // NOTE: This should never happen. Adding for exhaustiveness (CJP).
1018- return Promise . reject ( new Error ( 'Unexpected error in license key request' ) ) ;
924+ return Promise . reject ( new Error ( 'Unexpected error in app cert request' ) ) ;
1019925 }
1020926 return Promise . reject ( errOrResp ) ;
1021927 } ) ;
1022- await session . update ( response ) . catch ( ( ) => {
1023- // eslint-disable-next-line no-shadow
928+ await keys . setServerCertificate ( fairPlayAppCert ) . catch ( ( ) => {
1024929 const message = i18n (
1025- 'Failed to update DRM license . This may be an issue with the player or your protected content .'
930+ 'Your server certificate failed when attempting to set it . This may be an issue with a no longer valid certificate .'
1026931 ) ;
1027932 const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
1028933 mediaError . errorCategory = MuxErrorCategory . DRM ;
1029- mediaError . muxCode = MuxErrorCode . ENCRYPTED_UPDATE_LICENSE_FAILED ;
934+ mediaError . muxCode = MuxErrorCode . ENCRYPTED_UPDATE_SERVER_CERT_FAILED ;
1030935 return Promise . reject ( mediaError ) ;
1031936 } ) ;
1032937 // @ts -ignore
1033938 } catch ( error : Error | MediaError ) {
1034939 saveAndDispatchError ( mediaEl , error ) ;
1035940 return ;
1036941 }
942+ await mediaEl . setMediaKeys ( keys ) ;
943+ } ;
944+
945+ const updateMediaKeyStatus = ( mediaKeyStatus : MediaKeyStatus ) => {
946+ let mediaError ;
947+ if ( mediaKeyStatus === 'internal-error' ) {
948+ const message = i18n (
949+ 'The DRM Content Decryption Module system had an internal failure. Try reloading the page, upading your browser, or playing in another browser.'
950+ ) ;
951+ mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
952+ mediaError . errorCategory = MuxErrorCategory . DRM ;
953+ mediaError . muxCode = MuxErrorCode . ENCRYPTED_CDM_ERROR ;
954+ } else if ( mediaKeyStatus === 'output-restricted' || mediaKeyStatus === 'output-downscaled' ) {
955+ const message = i18n (
956+ 'DRM playback is being attempted in an environment that is not sufficiently secure. User may see black screen.'
957+ ) ;
958+ // NOTE: When encountered, this is a non-fatal error (though it's certainly interruptive of standard playback experience). (CJP)
959+ mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , false ) ;
960+ mediaError . errorCategory = MuxErrorCategory . DRM ;
961+ mediaError . muxCode = MuxErrorCode . ENCRYPTED_OUTPUT_RESTRICTED ;
962+ }
963+
964+ if ( mediaError ) {
965+ saveAndDispatchError ( mediaEl , mediaError ) ;
966+ }
967+ } ;
968+
969+ const setupMediaKeySession = async ( initDataType : string , initData : ArrayBuffer ) => {
970+ const session = ( mediaEl . mediaKeys as MediaKeys ) . createSession ( ) ;
971+ const onKeyStatusChange = ( ) => {
972+ // recheck key statuses
973+ // NOTE: As an improvement, we could also add checks for a status of 'expired' and
974+ // attempt to renew the license here (CJP)
975+ session . keyStatuses . forEach ( ( keyStatus ) => updateMediaKeyStatus ( keyStatus ) ) ;
976+ } ;
977+
978+ const onMessage = async ( event : MediaKeyMessageEvent ) => {
979+ const spc = event . message ;
980+ try {
981+ const ckc = await getLicenseKey ( spc , toLicenseKeyURL ( props , 'fairplay' ) ) ;
982+
983+ try {
984+ // This is the same call whether we are local or AirPlay.
985+ // Safari will forward CKC to Apple TV automatically.
986+ await session . update ( ckc ) ;
987+ } catch {
988+ const message = i18n (
989+ 'Failed to update DRM license. This may be an issue with the player or your protected content.'
990+ ) ;
991+ const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
992+ mediaError . errorCategory = MuxErrorCategory . DRM ;
993+ mediaError . muxCode = MuxErrorCode . ENCRYPTED_UPDATE_LICENSE_FAILED ;
994+
995+ saveAndDispatchError ( mediaEl , mediaError ) ;
996+ }
997+ } catch ( errOrResp ) {
998+ if ( errOrResp instanceof Response ) {
999+ const mediaError = getErrorFromResponse ( errOrResp , MuxErrorCategory . DRM , props ) ;
1000+ console . error ( 'mediaError' , mediaError ?. message , mediaError ?. context ) ;
1001+
1002+ if ( mediaError ) {
1003+ saveAndDispatchError ( mediaEl , mediaError ) ;
1004+ return ;
1005+ }
1006+
1007+ console . error ( 'Unexpected error in license key request' , errOrResp ) ;
1008+ return ;
1009+ }
1010+
1011+ console . error ( errOrResp ) ;
1012+ }
1013+ } ;
1014+
1015+ session . addEventListener ( 'keystatuseschange' , onKeyStatusChange ) ;
1016+ session . addEventListener ( 'message' , onMessage ) ;
1017+ mediaEl . addEventListener (
1018+ 'teardown' ,
1019+ ( ) => {
1020+ session . removeEventListener ( 'keystatuseschange' , onKeyStatusChange ) ;
1021+ session . removeEventListener ( 'message' , onMessage ) ;
1022+ session . close ( ) ;
1023+ } ,
1024+ { once : true }
1025+ ) ;
1026+
1027+ await session . generateRequest ( initDataType , initData ) . catch ( ( e ) => {
1028+ console . error ( 'Failed to generate license request' , e ) ;
1029+ const message = i18n (
1030+ 'Failed to generate a DRM license request. This may be an issue with the player or your protected content.'
1031+ ) ;
1032+ const mediaError = new MediaError ( message , MediaError . MEDIA_ERR_ENCRYPTED , true ) ;
1033+ mediaError . errorCategory = MuxErrorCategory . DRM ;
1034+ mediaError . muxCode = MuxErrorCode . ENCRYPTED_GENERATE_REQUEST_FAILED ;
1035+ return Promise . reject ( mediaError ) ;
1036+ } ) ;
1037+ } ;
1038+
1039+ const onFpEncrypted = async ( event : MediaEncryptedEvent ) => {
1040+ try {
1041+ const initDataType = event . initDataType ;
1042+ if ( initDataType !== 'skd' ) {
1043+ console . error ( `Received unexpected initialization data type "${ initDataType } "` ) ;
1044+ return ;
1045+ }
1046+
1047+ if ( ! mediaEl . mediaKeys ) {
1048+ await setupMediaKeys ( initDataType ) ;
1049+ }
1050+
1051+ const initData = event . initData ;
1052+ if ( initData == null ) {
1053+ console . error ( `Could not start encrypted playback due to missing initData in ${ event . type } event` ) ;
1054+ return ;
1055+ }
1056+
1057+ await setupMediaKeySession ( initDataType , initData ) ;
1058+ // @ts -ignore
1059+ } catch ( error : Error | MediaError ) {
1060+ saveAndDispatchError ( mediaEl , error ) ;
1061+ return ;
1062+ }
10371063 } ;
10381064
10391065 addEventListenerWithTeardown ( mediaEl , 'encrypted' , onFpEncrypted ) ;
0 commit comments