import React, { useRef, useState, useEffect } from "react"; import Hls from "hls.js"; function getPosterSrc(playbackId, options = {}) { const width = options.width || 640; const height = options.height || ""; const time = options.time || 0; const fitMode = typeof options.fitMode === "undefined" ? "smartcrop" : options.fitMode; let url = `https://image.mux.com/${playbackId}/thumbnail.png?width=${width}&fit_mode=${fitMode}&time=${time}`; if (options.height) { url += `&height=${height}`; } return url; } export default React.memo(function Video({ autoload = true, autoplay = false, className = "", loop = false, muted = false, playsinline, showControls = false, poster = true, settings = {}, assetDocument, onAttached }) { const hls = useRef(); const video = useRef(); const container = useRef(); const [posterUrl, setPosterUrl] = useState(); const [errors, setErrors] = useState(); const [source, setSource] = useState(); const hlsjsDefaults = { debug: process.env.NODE_ENV !== "production", enableWorker: true, lowLatencyMode: true, backBufferLength: 60 * 1 }; const attachVideo = () => { settings.autoStartLoad = autoload; if (Hls.isSupported()) { const hlsConfig = Object.assign(settings, hlsjsDefaults); hls.current = new Hls(hlsConfig); hls.current.loadSource(source); hls.current.attachMedia(video.current); hls.current.on(Hls.Events.MANIFEST_PARSED, () => { if (container.current) { container.current.style.display = "block"; } if (onAttached) { onAttached({ video: video.current }); } }); hls.current.on(Hls.Events.ERROR, (event, data) => { switch (data.type) { case Hls.ErrorTypes.NETWORK_ERROR: container.current.style.display = "none"; setErrors({ error: data }); break; case Hls.ErrorTypes.MEDIA_ERROR: // Don't output anything visible as these mostly are non-fatal break; default: container.current.style.display = "none"; setErrors({ error: data }); } console.error(data); // eslint-disable-line no-console }); } if (video.current.canPlayType("application/vnd.apple.mpegurl")) { video.current.src = source; video.current.addEventListener("loadedmetadata", () => { container.current.style.display = "block"; }); video.current.addEventListener("error", () => { container.current.style.display = "none"; setErrors({ error: { type: `${video.current.error.constructor.name} code ${video.current.error.code}` } }); }); } addVideoEventListeners(video.current); }; function handleVideoEvent(evt) { switch (evt.type) { case "error": data = Math.round(evt.target.currentTime * 1000); if (evt.type === "error") { let errorTxt; const mediaError = evt.currentTarget.error; switch (mediaError.code) { case mediaError.MEDIA_ERR_ABORTED: errorTxt = "You aborted the video playback"; break; case mediaError.MEDIA_ERR_DECODE: errorTxt = "The video playback was aborted due to a corruption problem or because the video used features your browser did not support"; handleMediaError(); break; case mediaError.MEDIA_ERR_NETWORK: errorTxt = "A network error caused the video download to fail part-way"; break; case mediaError.MEDIA_ERR_SRC_NOT_SUPPORTED: errorTxt = "The video could not be loaded, either because the server or network failed or because the format is not supported"; break; } if (mediaError.message) { errorTxt += " - " + mediaError.message; } //try to recover hls.current.recoverMediaError() console.error(errorTxt); } break; default: break; } } const addVideoEventListeners = elem => { elem.addEventListener("error", handleVideoEvent); }; const setup = () => { if (assetDocument && assetDocument.playbackId) { if (poster === true) { const rect = container.current.getBoundingClientRect(); const width = rect.width > 768 ? 640 : 400; const url = getPosterSrc(assetDocument.playbackId, { time: assetDocument.thumbTime || 0, fitMode: "preserve", width }); setPosterUrl(url); } setSource(`https://stream.mux.com/${assetDocument.playbackId}.m3u8`); } }; const destroy = () => { return new Promise((resolve, reject) => { setErrors(); if (hls.current) { hls.current.destroy(); clearInterval(hls.current?.bufferTimer); hls.current = null; } resolve(); }); }; const checkShouldLoad = () => { if (source && video.current && !video.current.src) { attachVideo(); return; } destroy(); }; useEffect(() => { setup(); }, []); useEffect(() => { checkShouldLoad(); }, [source]); useEffect(() => { if (errors) { console.error(errors); } }, [errors]); return (