-
Notifications
You must be signed in to change notification settings - Fork 61
chore: Add an application level player react component that uses hls.js directly and supports Mux Video DRM. #1168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
2e74f09
6f27f60
b7845d8
9530e63
8166221
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,7 +2,6 @@ build | |
coverage | ||
dist | ||
node_modules | ||
examples | ||
*.sublime-* | ||
*.vscode* | ||
*.md |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
-----BEGIN PRIVATE KEY----- | ||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDGCe0MWB02oNTY | ||
UIwcXh8HR6ryYE6eACVriOK12OeIwhKnR+DLyVwCMvcxhP8cnQNAFtv5m3GLkIqC | ||
IQlN/LEoJgO0keer63PvMpAAngXEPB5DbBDA4HouNDPKRncWONYry3DxafCFV0GF | ||
FXIPpnkEoAa7WthnawTY0L+RxCNKYPLLr1ysISfwQtT/iI44qY0yceBrHlzJcXHm | ||
X4WzvM2SHBBrSxwQiDq3s1kbZG/jh3m7bPiaDi+dAtw13nAPrcvr+5y8InKQlHKg | ||
fZMDUsqDLsYWTPRWKfNcuHmrzh09nemk59qEydWs4FfmCingga1/pPm8J/9v7QOR | ||
bV5zegRnAgMBAAECggEAfer1xDGKcZ+VdezRCYB27iQNd4rwsdmZ8WPZ8pH1tajg | ||
P2iecDSZwfQx5FSR+NmEpY1jVLQXYAwJZXAoAGQ5KDPziy5yRYex8VcO5LWVKQ6o | ||
l2nwxRJ13Ubn3ycqtQByfOof5//aJI40ZLAcFANfUyeE4D+E9UzAijNAeyhg3/rg | ||
YSoKEG9gvJzedXclawgASOq1yvnFeOT7IVwRg1mKbQ0noyDtKZIUo4pCK2/l5PF5 | ||
5ZYQwD97xgGfS/I6yYZlnUgq0TH8gioHCyDI+hNLMlCtUeHlENU3Ysdc3xACfdP4 | ||
dp7Cy2d+M0RamYrGEpHu7TT/ZhNA5uomk8KbTZmkoQKBgQDIc3XPzDBsbJt0tNoq | ||
mOtXfWwZ6wgaC7/L8eO+/SUVupCe64mNkIXKtAPNIzMp2+WhKWscHhIv4iMQrDA4 | ||
HSlA5pRdTdvt7Nrv5N92AzhXygr9DjLXyVPOduN8HpRBOWR0URgSIgFh+syKlo8q | ||
7+vJAdYd8jXWAJ6XkHj3C3gl7wKBgQD861WuFWq5Gye4QJ7df3GqdwpthQqSsELu | ||
B0/n/XUrHXkCM5XcunnN/OKv5gtMafTwGxG1nlmHD/QGFo3Zm++nffomlx/2mcf6 | ||
NGmPogtmqOWqE8RnXH9lfZvh7Ya3rCkaPXdhmp0LWX8w+7+PTdHCCz6OXJyv9tzO | ||
lUBxFlFBCQKBgGqHGexGGtH0YiWC25LZ0/CaIjIf+x1Ecziio6NjiyriDGu4x1Bp | ||
pwDT7FU/yLgNOhsNFPRLcuTprDL3H2Ui8kKgh+aSMzhdsPjezHc+PNpC8NYNjq2p | ||
PBW0jy7uXWHQa3d5hW5VjiCRFdTtMMbj3I3loPInP78scxwfVnoMKV0XAoGBAIG5 | ||
972N8Kq3Wf0w8Atuhg/IdUnNlqm8zOeoSn0UzRdrS5ksem64Gyfj0SYl2Z+9LUxG | ||
piPA7+zN6v2Abguy5w7DGB7ZHyTupdsZLRfmJvDmKr682t2lXRbigaU1nwbwwDhc | ||
VLJ0ip66rfmi9xN6998Ow2xj5l5/QayYc3BN4Sl5AoGAUe/Y9OeUmEh3Dq+CRyhs | ||
Lxd9/+iU6Zyhm9mxOaIbXpp+OwujdG0qc7WKGLM4Wh15SW0BUjSnmNCKE9dlLQeR | ||
UVy0Sog82mnCNrkHSz/bml0DbE0lUkizonOQz8oCgDkkfE/DRzMQYn52NyJ4YjRS | ||
TCsc0dPlXDm0VB7jGwy6QWs= | ||
-----END PRIVATE KEY----- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
-----BEGIN CERTIFICATE----- | ||
MIIEaDCCAtCgAwIBAgIRAKC0znDOShrNYYEqvpF/at8wDQYJKoZIhvcNAQELBQAw | ||
gYsxHjAcBgNVBAoTFW1rY2VydCBkZXZlbG9wbWVudCBDQTEwMC4GA1UECwwnY3Bp | ||
bGxzYnVyeUBDaHJpc3RpYW5zLU1hY0Jvb2stUHJvLmxvY2FsMTcwNQYDVQQDDC5t | ||
a2NlcnQgY3BpbGxzYnVyeUBDaHJpc3RpYW5zLU1hY0Jvb2stUHJvLmxvY2FsMB4X | ||
DTI1MDcxNDE4MzY0NVoXDTI3MTAxNDE4MzY0NVowWzEnMCUGA1UEChMebWtjZXJ0 | ||
IGRldmVsb3BtZW50IGNlcnRpZmljYXRlMTAwLgYDVQQLDCdjcGlsbHNidXJ5QENo | ||
cmlzdGlhbnMtTWFjQm9vay1Qcm8ubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IB | ||
DwAwggEKAoIBAQDGCe0MWB02oNTYUIwcXh8HR6ryYE6eACVriOK12OeIwhKnR+DL | ||
yVwCMvcxhP8cnQNAFtv5m3GLkIqCIQlN/LEoJgO0keer63PvMpAAngXEPB5DbBDA | ||
4HouNDPKRncWONYry3DxafCFV0GFFXIPpnkEoAa7WthnawTY0L+RxCNKYPLLr1ys | ||
ISfwQtT/iI44qY0yceBrHlzJcXHmX4WzvM2SHBBrSxwQiDq3s1kbZG/jh3m7bPia | ||
Di+dAtw13nAPrcvr+5y8InKQlHKgfZMDUsqDLsYWTPRWKfNcuHmrzh09nemk59qE | ||
ydWs4FfmCingga1/pPm8J/9v7QORbV5zegRnAgMBAAGjdjB0MA4GA1UdDwEB/wQE | ||
AwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAfBgNVHSMEGDAWgBRZEmSxNIWPLl2V | ||
EgeR3bfAE1KlqDAsBgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAA | ||
AAAAAAAAAAEwDQYJKoZIhvcNAQELBQADggGBAEuhrXSh1NpIteqcUJ7fmRFfFsxc | ||
/4EhWWtrXv/TEmitVROQR1+MQ/ImZUL9EzruZl7qxbmoqufoA41645ez666kRy5U | ||
tD6/Ide/X01LyT+V5S26RxfQIHZKEHaXrwM1vMoceso6MwUIEsX1jsyd3/XoMwOy | ||
+1Oe1CVzfwa8kckT8QUWniEiJRX+DLOp3VFEkcsaXzV9snGI6e97CoskJuZdeyBK | ||
W2SVXqHXK5lz4h7dRTGV0MqprwZLE4dUyBQVVr0MpRaHJ1YktSbdMAxvJiSRfUFY | ||
d0WgmhnCtN1drY5ZT2oTY2SvvP6xJUgdhTEKNyLtFSQBNTtUH1iZqTTl/F0xiaLw | ||
8NWNXUjsFzJky0rNNajOVLGEOct45r+p4KTyYvaHeHIiLmKn8igOmUV0Nt3rH8t+ | ||
6vpXguUc0IZFwuphe0I5dKQy1dzyWDgw4CMFEYxy+SZ0OJ9xP6T4T3rQkKveNnI1 | ||
3gCuGmf2IhhAvx0AJ61PEMD3EhR4ULKKRiTzsg== | ||
-----END CERTIFICATE----- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,153 @@ | ||
/* eslint-disable no-console */ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a SimpleVideoPlayer that use hls.js and adds Mux Video DRM + playback token support. |
||
import type { DRMSystemsConfiguration } from 'hls.js'; | ||
import HLS from 'hls.js'; | ||
import { useEffect, useRef } from 'react'; | ||
|
||
// import type { Props } from '../types'; | ||
type Props = { playbackId: string; playbackData: PlaybackData }; | ||
|
||
/** | ||
* Represents the data needed to play a video. | ||
* | ||
* For videos protected by DRM, both tokens are needed. The playback token | ||
* provides access and the DRM token allows for decryption. | ||
* | ||
* Playback policies control who can view content, while DRM provides additional | ||
* encryption to prevent unauthorized copying. | ||
* | ||
* @see https://www.mux.com/docs/guides/secure-video-playback | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm | ||
*/ | ||
export interface PlaybackData { | ||
/** | ||
* Unique identifier for the video asset. | ||
*/ | ||
playbackId: string; | ||
/** | ||
* URL of the video including the playback token. | ||
*/ | ||
tokens: { | ||
/** | ||
* JSON Web Token (JWT) used to acquire the DRM license. | ||
*/ | ||
drm?: string; | ||
/** | ||
* JSON Web Token (JWT) used to authenticate the signed playback policy. | ||
*/ | ||
playback?: string; | ||
}; | ||
} | ||
|
||
/** | ||
* The types of licenses that Mux supports. | ||
* | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm#generic-player-integration | ||
*/ | ||
type LicenseType = 'widevine' | 'fairplay' | 'playready'; | ||
|
||
/** | ||
* Constructs the Mux-specific license URL for the given license type for the | ||
* given playback data. | ||
* | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm#generic-player-integration | ||
*/ | ||
const licenseUrlFor = (type: LicenseType, { playbackId, tokens: { drm: drmToken } }: PlaybackData) => { | ||
return `https://license.mux.com/license/${type}/${playbackId}?token=${drmToken}`; | ||
}; | ||
|
||
/** | ||
* Constructs the Mux-specific server certificate URL for the given license | ||
* type for the given playback data. | ||
* | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm#generic-player-integration | ||
*/ | ||
const serverCertificateUrlFor = (type: LicenseType, { playbackId, tokens: { drm: drmToken } }: PlaybackData) => { | ||
return `https://license.mux.com/appcert/${type}/${playbackId}?token=${drmToken}`; | ||
}; | ||
|
||
/** | ||
* Returns the HLS DRM systems configuration for the given playback data. The | ||
* fields we need to provide in this configuration are determined by what Mux | ||
* supports. | ||
* | ||
* @see https://github.com/video-dev/hls.js/blob/master/docs/API.md#drmsystems | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm#widevine | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm#fairplay | ||
* @see https://www.mux.com/docs/guides/protect-videos-with-drm#playready | ||
*/ | ||
export const drmSystemsConfig = (playbackData: PlaybackData): DRMSystemsConfiguration => ({ | ||
'com.widevine.alpha': { | ||
licenseUrl: licenseUrlFor('widevine', playbackData), | ||
}, | ||
'com.apple.fps': { | ||
licenseUrl: licenseUrlFor('fairplay', playbackData), | ||
serverCertificateUrl: serverCertificateUrlFor('fairplay', playbackData), | ||
}, | ||
'com.microsoft.playready': { | ||
licenseUrl: licenseUrlFor('playready', playbackData), | ||
}, | ||
}); | ||
|
||
export const SimpleVideoPlayer = (props: PlaybackData) => { | ||
const { playbackId, tokens: { playback: playbackToken, drm: drmToken } = {} } = props; | ||
const videoRef = useRef<HTMLVideoElement>(null); | ||
const videoUrl = `https://stream.mux.com/${playbackId}.m3u8?token=${playbackToken}`; | ||
|
||
useEffect(() => { | ||
if (!videoRef.current) { | ||
return; | ||
} | ||
|
||
if (!HLS.isSupported()) { | ||
videoRef.current.src = videoUrl; | ||
return; | ||
} | ||
|
||
const hls = new HLS({ | ||
debug: true, | ||
|
||
/** | ||
* When there is playback data, we're using Mux's DRM system, so we | ||
* need to enable Encrypted Media Extensions (EME). | ||
* | ||
* @see https://en.wikipedia.org/wiki/Encrypted_Media_Extensions | ||
* @see https://developer.mozilla.org/en-US/docs/Web/API/Encrypted_Media_Extensions_API | ||
* @see https://github.com/video-dev/hls.js/blob/master/docs/API.md#emeenabled | ||
*/ | ||
emeEnabled: !!drmToken, | ||
|
||
/** | ||
* Describes the DRM systems that are supported by our DRM provider (Mux). | ||
* | ||
* @see https://github.com/video-dev/hls.js/blob/master/docs/API.md#drmsystems | ||
*/ | ||
drmSystems: drmToken ? drmSystemsConfig(props) : {}, | ||
}); | ||
|
||
hls.loadSource(videoUrl); | ||
hls.attachMedia(videoRef.current); | ||
|
||
hls.on(HLS.Events.ERROR, (event, data) => { | ||
if (data.fatal) { | ||
switch (data.type) { | ||
case HLS.ErrorTypes.NETWORK_ERROR: | ||
console.log('Network error occurred', event, data); | ||
break; | ||
case HLS.ErrorTypes.MEDIA_ERROR: | ||
console.log('Media error occurred', event, data); | ||
break; | ||
case HLS.ErrorTypes.OTHER_ERROR: | ||
console.log('An unknown error occurred', event, data); | ||
break; | ||
} | ||
} | ||
}); | ||
}, [props, videoUrl]); | ||
|
||
return ( | ||
<video ref={videoRef} controls> | ||
<track kind="captions" /> | ||
</video> | ||
); | ||
}; | ||
/* eslint-enable no-console */ |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import Head from 'next/head'; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a test page for using the |
||
import type { NextParsedUrlQuery } from 'next/dist/server/request-meta'; | ||
import { PlaybackData, SimpleVideoPlayer } from '../components/SimpleVideoPlayer'; | ||
import { TextRenderer } from '../components/renderers'; | ||
import URLPathRenderer from '../components/URLPathRenderer'; | ||
import ComponentCodeRenderer from '../components/ComponentCodeRenderer'; | ||
import { LocationProps, getLocationServerSideProps, usePageStateReducer } from '../app/page-state'; | ||
|
||
const DEFAULT_INITIAL_STATE: Partial<PlaybackData> = Object.freeze({ | ||
tokens: undefined, | ||
playbackId: undefined, | ||
}); | ||
|
||
const toInitialState = (query: NextParsedUrlQuery) => { | ||
const queryState = Object.fromEntries(Object.entries(query).map(([k, v]) => [k, JSON.parse(v as string)])); | ||
const initialState = { | ||
...DEFAULT_INITIAL_STATE, | ||
...queryState, | ||
}; | ||
return initialState as Partial<PlaybackData>; | ||
}; | ||
|
||
export const getServerSideProps = getLocationServerSideProps; | ||
|
||
type Props = LocationProps; | ||
|
||
function MuxPlayerPage({ location }: Props) { | ||
/** @TODO fix typing complexities here (CJP) */ | ||
// @ts-ignore | ||
const [state, _dispatch, genericOnChange] = usePageStateReducer<PlaybackData>(toInitialState, [undefined]); | ||
|
||
return ( | ||
<> | ||
<Head> | ||
<title><MuxPlayer/> Demo</title> | ||
</Head> | ||
<main className="component-page"> | ||
<SimpleVideoPlayer | ||
// ref={mediaElRef} | ||
// Test _hlsConfig for MuxPlayer (react) (Note: This also indirectly tests <mux-player> & <mux-video>) | ||
// _hlsConfig={{ | ||
// startLevel: 2, | ||
// debug: true, | ||
// }} | ||
playbackId={state.playbackId} | ||
tokens={state.tokens} | ||
/> | ||
|
||
<div className="options"> | ||
<ComponentCodeRenderer state={state} component="SimpleVideoPlayerPlayer" /> | ||
<URLPathRenderer state={state} location={typeof window !== 'undefined' ? window.location : location} /> | ||
<div> | ||
<h2>Manual Config</h2> | ||
</div> | ||
<TextRenderer value={state.playbackId} name="playbackId" onChange={genericOnChange} /> | ||
<TextRenderer value={state.tokens?.playback} name="tokens.playback" onChange={genericOnChange} /> | ||
<TextRenderer value={state.tokens?.drm} name="tokens.drm" onChange={genericOnChange} /> | ||
</div> | ||
</main> | ||
</> | ||
); | ||
} | ||
|
||
export default MuxPlayerPage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NOTE: These are locally created keys for testing https servers locally (a pre-requisite for some DRM test cases). No concerns with these being pushed to remote repo.