Skip to content

Commit

Permalink
Merge pull request #175 from GoogleChrome/add/cast
Browse files Browse the repository at this point in the history
Google Cast
  • Loading branch information
derekherman authored Jan 20, 2022
2 parents 10ff66b + 20d1bfb commit cd26b29
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 16 deletions.
3 changes: 2 additions & 1 deletion cors.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
"https://kinoweb-dev.web.app",
"https://kinoweb-dev--staging-2ra3ji0i.web.app",
"https://kinoweb.dev",
"http://localhost:5000"
"http://localhost:5000",
"https://www.gstatic.com"
],
"responseHeader": [
"Accept-Ranges",
Expand Down
2 changes: 1 addition & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"headers": [
{
"key": "Content-Security-Policy",
"value": "script-src 'self'; object-src 'none'; base-uri 'none'"
"value": "script-src 'self' www.gstatic.com; object-src 'none'; base-uri 'none'"
}
]
}],
Expand Down
1 change: 1 addition & 0 deletions src/api/01-basics/01-single-video.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ length: '1:04'
video-sources:
- src: https://storage.googleapis.com/kino-assets/single-video/video.mp4
type: video/mp4; codecs="avc1.640032,mp4a.40.2"
cast: true
thumbnail: https://storage.googleapis.com/kino-assets/single-video/thumbnail.png
media-session-artwork:
- sizes: 96x96
Expand Down
1 change: 1 addition & 0 deletions src/api/01-basics/02-multiple-sources.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ video-sources:
type: video/mp4; codecs="av01.0.04M.08, mp4a.40.2"
- src: https://storage.googleapis.com/kino-assets/multiple-sources/hevc.mp4
type: video/mp4; codecs="hev1.1.6.L93.90,mp4a.40.2"
cast: true
- src: https://storage.googleapis.com/kino-assets/multiple-sources/vp9.webm
type: video/webm
thumbnail: https://storage.googleapis.com/kino-assets/multiple-sources/thumbnail.png
Expand Down
5 changes: 3 additions & 2 deletions src/api/01-basics/03-using-webvtt.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ video-sources:
type: video/mp4; codecs="av01.0.04M.08, mp4a.40.2"
- src: https://storage.googleapis.com/kino-assets/using-webvtt/hevc.mp4
type: video/mp4; codecs="hev1.1.6.L93.90,mp4a.40.2"
cast: true
- src: https://storage.googleapis.com/kino-assets/using-webvtt/vp9.webm
type: video/webm
video-subtitles:
Expand All @@ -21,8 +22,8 @@ video-subtitles:
- default: false
kind: captions
label: Česky
src: https://storage.googleapis.com/kino-assets/using-webvtt/cap-cz.vtt
srclang: cz
src: https://storage.googleapis.com/kino-assets/using-webvtt/cap-cs.vtt
srclang: cs
thumbnail: https://storage.googleapis.com/kino-assets/using-webvtt/thumbnail.png
media-session-artwork:
- sizes: 96x96
Expand Down
5 changes: 3 additions & 2 deletions src/api/02-streaming/04-streaming-basics.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ length: '1:04'
video-sources:
- src: https://storage.googleapis.com/kino-assets/streaming-basics/manifest.mpd
type: application/dash+xml
cast: true
- src: https://storage.googleapis.com/kino-assets/streaming-basics/master.m3u8
type: application/x-mpegURL
url-rewrites:
Expand All @@ -22,8 +23,8 @@ video-subtitles:
- default: false
kind: captions
label: Česky
src: https://storage.googleapis.com/kino-assets/streaming-basics/cap-cz.vtt
srclang: cz
src: https://storage.googleapis.com/kino-assets/streaming-basics/cap-cs.vtt
srclang: cs
thumbnail: https://storage.googleapis.com/kino-assets/streaming-basics/thumbnail.png
media-session-artwork:
- sizes: 96x96
Expand Down
5 changes: 3 additions & 2 deletions src/api/02-streaming/05-efficient-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ length: '1:04'
video-sources:
- src: https://storage.googleapis.com/kino-assets/efficient-formats/manifest.mpd
type: application/dash+xml
cast: true
- src: https://storage.googleapis.com/kino-assets/efficient-formats/master.m3u8
type: application/x-mpegURL
url-rewrites:
Expand All @@ -22,8 +23,8 @@ video-subtitles:
- default: false
kind: captions
label: Česky
src: https://storage.googleapis.com/kino-assets/efficient-formats/cap-cz.vtt
srclang: cz
src: https://storage.googleapis.com/kino-assets/efficient-formats/cap-cs.vtt
srclang: cs
thumbnail: https://storage.googleapis.com/kino-assets/efficient-formats/thumbnail.png
media-session-artwork:
- sizes: 96x96
Expand Down
5 changes: 3 additions & 2 deletions src/api/02-streaming/06-adaptive-streaming.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ length: '1:04'
video-sources:
- src: https://storage.googleapis.com/kino-assets/adaptive-streaming/manifest.mpd
type: application/dash+xml
cast: true
- src: https://storage.googleapis.com/kino-assets/adaptive-streaming/master.m3u8
type: application/x-mpegURL
url-rewrites:
Expand All @@ -22,8 +23,8 @@ video-subtitles:
- default: false
kind: captions
label: Česky
src: https://storage.googleapis.com/kino-assets/adaptive-streaming/cap-cz.vtt
srclang: cz
src: https://storage.googleapis.com/kino-assets/adaptive-streaming/cap-cs.vtt
srclang: cs
thumbnail: https://storage.googleapis.com/kino-assets/adaptive-streaming/thumbnail.png
media-session-artwork:
- sizes: 96x96
Expand Down
47 changes: 47 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,50 @@ if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js');
});
}

/**
* Create a global closure that ensures that Google Cast
* is only initialized once and allows the cast button
* to be reused across pages.
*/
window.kinoInitGoogleCast = (function kinoInitGoogleCastIIFE() {
let castButtonPromise = false;

/**
* Initializes Google Cast Web Sender and returns a promise resolving
* with a cast button.
*
* @returns {Promise<HTMLElement>} Custom <google-cast-launcher> element.
*/
return () => {
if (!castButtonPromise) {
castButtonPromise = new Promise((resolve) => {
const initCastApi = () => {
window.cast.framework.CastContext.getInstance().setOptions({
receiverApplicationId: window.chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID,
});

const castButton = document.createElement('button');
const castCustomElement = document.createElement('google-cast-launcher');

castButton.setAttribute('aria-label', 'Cast this video');
castButton.appendChild(castCustomElement);

resolve(castButton);
};

window.__onGCastApiAvailable = (isAvailable) => {
if (isAvailable) {
initCastApi();
}
};

const scriptEl = document.createElement('script');
scriptEl.type = 'text/javascript';
scriptEl.src = 'https://www.gstatic.com/cv/js/sender/v1/cast_sender.js?loadCastFramework=1';
document.head.appendChild(scriptEl);
});
}
return castButtonPromise;
};
}());
7 changes: 7 additions & 0 deletions src/js/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -130,3 +130,10 @@ export const IDB_DATA_CHANGED_EVENT = 'idb-data-changed';
* Picture in picture.
*/
export const PIP_CLASSNAME = 'picture-in-picture';

/**
* Casting.
*/
export const CAST_CLASSNAME = 'cast';
export const CAST_HAS_TARGET_NAME = 'cast-has-target';
export const CAST_TARGET_NAME = 'cast-target-name';
30 changes: 25 additions & 5 deletions src/js/web-components/video-player/VideoPlayer.css
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
position: absolute;
top: 16px;
right: 16px;
z-index: 2;
z-index: auto; /* In order to not interfere with the mobile slide out menu. */
}

@media (min-width: 720px) {
Expand All @@ -27,7 +27,7 @@
}
}

:host .floating-buttons button {
:host .floating-buttons > * {
border-radius: 8px;
background-color: var(--accent-background);
width: 48px;
Expand All @@ -38,12 +38,19 @@
border: none;
}

button google-cast-launcher {
height: 24px;
width: auto;
--connected-color: var(--accent);
}

video {
width: 100%;
height: auto;
}

.pip-overlay {
.pip-overlay,
.cast-overlay {
display: none;
position: absolute;
inset: 0;
Expand All @@ -53,20 +60,33 @@ video {
font-size: clamp(12px, 4vw, 24px);
}

.pip-overlay svg {
.pip-overlay svg,
.cast-overlay svg {
align-self: end;
width: clamp(40px, 20vw, 128px);
height: auto;
}

:host(.picture-in-picture) .pip-overlay {
:host(.picture-in-picture) .pip-overlay,
:host(.cast) .cast-overlay {
display: grid;
justify-items: center;
row-gap: 16px;
}

.cast-overlay .cast-target {
display: none;
}

:host(.cast-has-target) .cast-overlay .cast-target {
display: inline;
}

@media (min-width: 720px) {
:host(.picture-in-picture) .pip-overlay {
row-gap: 32px;
}
:host(.cast) .cast-overlay {
row-gap: 32px;
}
}
88 changes: 87 additions & 1 deletion src/js/web-components/video-player/VideoPlayer.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ import ParserMPD from '../../classes/ParserMPD';
import selectSource from '../../utils/selectSource';

import {
CAST_CLASSNAME,
CAST_HAS_TARGET_NAME,
CAST_TARGET_NAME,
MEDIA_SESSION_DEFAULT_ARTWORK,
PIP_CLASSNAME,
} from '../../constants';
Expand Down Expand Up @@ -109,7 +112,11 @@ export default class extends HTMLElement {
<div class="floating-buttons"></div>
<div class="pip-overlay">
<svg viewBox="0 0 129 128" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M108.5 48V16a8.001 8.001 0 0 0-8-8h-84a8 8 0 0 0-8 8v68a8 8 0 0 0 8 8h20" stroke="var(--icon)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/><path d="M52.5 112V72a8 8 0 0 1 8-8h52a8 8 0 0 1 8 8v40a8 8 0 0 1-8 8h-52a8 8 0 0 1-8-8Z" stroke="var(--icon)" stroke-width="3" stroke-miterlimit="10" stroke-linecap="square"/></svg>
This video is playing in picture in picture
<p>This video is playing in picture in picture</p>
</div>
<div class="cast-overlay">
<svg viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg" xml:space="preserve" fill-rule="evenodd" clip-rule="evenodd" stroke-linecap="round" stroke-miterlimit="10"><path d="M34.133 107.201c0-13.253-10.747-24-24-24M53.333 107.2c0-23.861-19.339-43.2-43.2-43.2" fill="none" stroke="var(--icon)" stroke-width="8"/><path d="M10.133 112.001a4.8 4.8 0 1 0 0-9.6 4.8 4.8 0 0 0 0 9.6Z" fill="var(--icon)" fill-rule="nonzero"/><path d="M5.333 49.778V32c0-5.891 4.776-10.667 10.667-10.667h96c5.891 0 10.667 4.776 10.667 10.667v64c0 5.891-4.776 10.667-10.667 10.667H72.381" fill="none" stroke="var(--icon)" stroke-width="8"/></svg>
<p>Casting<span class="cast-target"> to <span class="cast-target-name"></span></span></p>
</div>
`;

Expand All @@ -131,6 +138,85 @@ export default class extends HTMLElement {
floatingButtonsBar.appendChild(pipButton);
}

window.kinoInitGoogleCast().then((castButton) => {
floatingButtonsBar.appendChild(castButton);

window.cast.framework.CastContext.getInstance().addEventListener(
window.cast.framework.CastContextEventType.SESSION_STATE_CHANGED,
async (e) => {
if (e.sessionState === 'SESSION_STARTED' || e.sessionState === 'SESSION_RESUMED') {
const castableSources = this.internal.videoData['video-sources'].filter((source) => source.cast === true);

if (!castableSources) {
/* eslint-disable-next-line no-console */
console.error('[Google Cast] The media has no source suitable for casting.');
return;
}

const castSession = window.cast.framework.CastContext.getInstance().getCurrentSession();
const mediaInfo = new window.chrome.cast.media.MediaInfo(
castableSources[0].src,
castableSources[0].type,
);
const videoThumbnail = new window.chrome.cast.Image(this.internal.videoData.thumbnail);
const metadata = new window.chrome.cast.media.GenericMediaMetadata();

metadata.title = videoData.title;

/**
* @todo Add the Media Session artwork and define image dimensions explicitly.
*/
metadata.images = [videoThumbnail];
mediaInfo.metadata = metadata;

/** @type {Array} */
const subtitles = this.internal.videoData['video-subtitles'] || [];
const defaultSubtitles = subtitles.find((subtitle) => subtitle.default);

/**
* AFAICT the Default Media Receiver doesn't implement any UI to
* select the subtitle track.
*
* We only add the subtitle track if there is a default one.
*/
if (defaultSubtitles) {
const defaultSubtitlesTrack = new window.chrome.cast.media.Track(
1,
window.chrome.cast.media.TrackType.TEXT,
);

defaultSubtitlesTrack.trackContentId = defaultSubtitles.src;
defaultSubtitlesTrack.subtype = window.chrome.cast.media.TextTrackType.SUBTITLES;
defaultSubtitlesTrack.name = defaultSubtitles.label;
defaultSubtitlesTrack.language = defaultSubtitles.srclang;
defaultSubtitlesTrack.trackContentType = 'text/vtt';

mediaInfo.tracks = [defaultSubtitlesTrack];
}

const request = new window.chrome.cast.media.LoadRequest(mediaInfo);

try {
await castSession.loadMedia(request);
} catch (error) {
/* eslint-disable-next-line no-console */
console.error(`[Google Cast] Error code: ${error}`);
return;
}

const targetName = castSession.getCastDevice().friendlyName;
this.internal.root.querySelector(`.${CAST_TARGET_NAME}`).innerText = targetName;
this.classList.toggle(CAST_HAS_TARGET_NAME, targetName);
this.classList.add(CAST_CLASSNAME);
}

if (e.sessionState === 'SESSION_ENDED') {
this.classList.remove(CAST_CLASSNAME);
}
},
);
});

/**
* Set up Media Session API integration.
*/
Expand Down

0 comments on commit cd26b29

Please sign in to comment.