Merge pull request #175 from GoogleChrome/add/cast

Google Cast
This commit is contained in:
Derek Herman 2022-01-20 00:12:57 -08:00 коммит произвёл GitHub
Родитель 10ff66bc71 20d1bfb822
Коммит cd26b29d6c
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
12 изменённых файлов: 183 добавлений и 16 удалений

Просмотреть файл

@ -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",

Просмотреть файл

@ -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'"
}
]
}],

Просмотреть файл

@ -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

Просмотреть файл

@ -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

Просмотреть файл

@ -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:
@ -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

Просмотреть файл

@ -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:
@ -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

Просмотреть файл

@ -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:
@ -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

Просмотреть файл

@ -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:
@ -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

Просмотреть файл

@ -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;
};
}());

Просмотреть файл

@ -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';

Просмотреть файл

@ -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) {
@ -27,7 +27,7 @@
}
}
:host .floating-buttons button {
:host .floating-buttons > * {
border-radius: 8px;
background-color: var(--accent-background);
width: 48px;
@ -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;
@ -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;
}
}

Просмотреть файл

@ -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';
@ -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>
`;
@ -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.
*/