UNPKG

dash-video-element

Version:

Custom element for playing video using the DASH format. Uses dash.js.

159 lines (157 loc) 6.34 kB
import { CustomVideoElement } from "custom-media-element"; import { MediaTracksMixin } from "media-tracks"; class DashVideoElement extends MediaTracksMixin(CustomVideoElement) { static shadowRootOptions = { ...CustomVideoElement.shadowRootOptions }; static getTemplateHTML = (attrs) => { const { src, ...rest } = attrs; return CustomVideoElement.getTemplateHTML(rest); }; #apiInit; attributeChangedCallback(attrName, oldValue, newValue) { if (attrName !== "src") { super.attributeChangedCallback(attrName, oldValue, newValue); } if (attrName === "src" && oldValue != newValue) { this.load(); } } async _initThumbnails(representation) { const generateAllCues = async (totalThumbnails2, thumbnailDuration2) => { const promises = []; const timescale = representation.timescale || 1; const startNumber = representation.startNumber || 1; const pto = representation.presentationTimeOffset ? representation.presentationTimeOffset / timescale : 0; const tduration = representation.segmentDuration; for (let thIndex = 0; thIndex < totalThumbnails2; thIndex++) { const startTime = calculateThumbnailStartTime({ thIndex, thduration: thumbnailDuration2, ttiles: totalThumbnails2, tduration, startNumber, pto }); const endTime = startTime + thumbnailDuration2; const promise = new Promise((resolve, reject) => { this.api.provideThumbnail(startTime, ({ url, width, height, x, y }) => { try { const cue = new VTTCue( startTime, endTime, `${url}#xywh=${x},${y},${width},${height}` ); resolve(cue); } catch (err) { reject(err); } }); }); promises.push(promise); } return await Promise.all(promises).catch((e) => console.error("Error processing thumbnails", e)); }; const { totalThumbnails, thumbnailDuration } = calculateThumbnailTimes(representation); const cues = await generateAllCues(totalThumbnails, thumbnailDuration); let track = this.nativeEl.querySelector('track[label="thumbnails"]'); if (!track) { track = createThumbnailTrack(); this.nativeEl.appendChild(track); const vttUrl = cuesToVttBlobUrl(cues); track.src = vttUrl; track.dispatchEvent(new Event("change")); } } async load() { if (this.#apiInit) { this.api.attachSource(this.src); return; } this.#apiInit = true; const Dash = await import("dashjs"); this.api = Dash.MediaPlayer().create(); this.api.initialize(this.nativeEl, this.src, this.autoplay); this.api.on(Dash.MediaPlayer.events.STREAM_INITIALIZED, () => { const bitrateList = this.api.getRepresentationsByType("video"); let videoTrack = this.videoTracks.getTrackById("main"); if (!videoTrack) { videoTrack = this.addVideoTrack("main"); videoTrack.id = "main"; videoTrack.selected = true; } bitrateList.forEach((rep) => { const bitrate = rep.bandwidth ?? rep.bitrate ?? (Number.isFinite(rep.bitrateInKbit) ? rep.bitrateInKbit * 1e3 : void 0); const rendition = videoTrack.addRendition(rep.id, rep.width, rep.height, rep.mimeType ?? rep.codec, bitrate); rendition.id = rep.id; }); this.videoRenditions.addEventListener("change", () => { const selected = this.videoRenditions[this.videoRenditions.selectedIndex]; if (selected == null ? void 0 : selected.id) { this.api.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: false } } } }); this.api.setRepresentationForTypeById("video", selected.id, true); } else { this.api.updateSettings({ streaming: { abr: { autoSwitchBitrate: { video: true } } } }); } }); if (!this.api.isDynamic()) { const imageReps = this.api.getRepresentationsByType("image"); imageReps.forEach(async (rep, idx) => { if (idx > 0) return; this._initThumbnails(rep); }); } }); } } function calculateThumbnailTimes(representation) { var _a, _b; const essentialProp = representation.essentialProperties[0]; const [htiles, vtiles] = essentialProp.value.split("x").map(Number); const ttiles = htiles * vtiles; const periodDuration = ((_b = (_a = representation.adaptation) == null ? void 0 : _a.period) == null ? void 0 : _b.duration) || null; const tileDuration = representation.segmentDuration; const timescale = representation.timescale || 1; const tduration = tileDuration / timescale; const thduration = tduration / ttiles; const totalThumbnails = periodDuration != null ? Math.ceil(periodDuration / thduration) : Math.ceil(tileDuration / thduration); return { totalThumbnails, thumbnailDuration: thduration }; } function calculateThumbnailStartTime({ thIndex, tduration, thduration, ttiles, startNumber, pto }) { const tnumber = Math.floor(thIndex / ttiles) + startNumber; const thnumber = thIndex % ttiles + 1; const tileStartTime = (tnumber - 1) * tduration - pto; const thumbnailStartTime = (thnumber - 1) * thduration; return tileStartTime + thumbnailStartTime; } function createThumbnailTrack() { const track = document.createElement("track"); track.kind = "metadata"; track.label = "thumbnails"; track.srclang = "en"; track.mode = "hidden"; track.default = true; return track; } function cuesToVttBlobUrl(cues) { let vtt = "WEBVTT\n\n"; for (const cue of cues) { vtt += `${formatTime(cue.startTime)} --> ${formatTime(cue.endTime)} `; vtt += `${cue.text} `; } const blob = new Blob([vtt], { type: "text/vtt" }); return URL.createObjectURL(blob); function formatTime(t) { const h = String(Math.floor(t / 3600)).padStart(2, "0"); const m = String(Math.floor(t % 3600 / 60)).padStart(2, "0"); const s = (t % 60).toFixed(3).padStart(6, "0"); return `${h}:${m}:${s}`; } } if (globalThis.customElements && !globalThis.customElements.get("dash-video")) { globalThis.customElements.define("dash-video", DashVideoElement); } var dash_video_element_default = DashVideoElement; export { dash_video_element_default as default };