UNPKG

video-stream-snap

Version:

Capture the first frame from video streams (HLS/RTMP/FLV)

333 lines (329 loc) 11.2 kB
"use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { default: () => index_default }); module.exports = __toCommonJS(index_exports); // src/utils.ts var createVideoElement = () => { const video = document.createElement("video"); video.crossOrigin = "anonymous"; video.muted = true; video.playsInline = true; video.preload = "auto"; video.style.cssText = ` position: fixed; opacity: 0; pointer-events: none; top: -1000px; `; return video; }; var isBlackFrame = (ctx, threshold = 20) => { const { width, height } = ctx.canvas; try { const imageData = ctx.getImageData(0, 0, width, height); const data = imageData.data; let sum = 0; for (let i = 0; i < data.length; i += 4) { sum += data[i] + data[i + 1] + data[i + 2]; } const avg = sum / (width * height * 3); return avg < threshold; } catch (e) { throw new Error("Canvas\u88AB\u6C61\u67D3\uFF0C\u8BF7\u786E\u4FDD\u89C6\u9891\u670D\u52A1\u5668\u542F\u7528CORS"); } }; var detectStreamType = (url) => { const lowerUrl = url.toLowerCase(); if (lowerUrl.includes(".m3u8")) return "hls"; if (lowerUrl.includes(".flv")) return "flv"; if (lowerUrl.includes(".mpd")) return "dash"; return "normal"; }; var checkCORS = async (url) => { try { const res = await fetch(url, { method: "HEAD", mode: "no-cors" }); return res.ok || false; } catch { return false; } }; // src/index.ts var import_flv = __toESM(require("flv.js")); var import_hls = __toESM(require("hls.js")); var dashjs = __toESM(require("dashjs")); if (typeof window !== "undefined") { window.flvjs = import_flv.default; window.Hls = import_hls.default; window.dashjs = dashjs; } var StreamSnap = class { static ensureCrossOrigin(video) { if (!video.crossOrigin) { video.crossOrigin = "anonymous"; video.src = ""; } } static async capture(streamUrl, options = {}) { const { timeout = 8e3, quality = 0.8, mimeType = "image/jpeg", skipBlackFrame = false, videoElement: existingVideo } = options; const video = existingVideo || createVideoElement(); this.ensureCrossOrigin(video); if (!existingVideo) { document.body.appendChild(video); } try { const isCORSEnabled = await checkCORS(streamUrl); if (!isCORSEnabled) { console.warn("CORS not enabled on server, attempting anyway..."); } const streamType = detectStreamType(streamUrl); switch (streamType) { case "flv": if (typeof window.flvjs !== "undefined") { return this.captureWithFLVJS(video, streamUrl, { timeout, quality, mimeType, skipBlackFrame }); } break; case "hls": if (typeof window.Hls !== "undefined") { return this.captureWithHLS(video, streamUrl, { timeout, quality, mimeType, skipBlackFrame }); } break; case "dash": if (typeof window.dashjs !== "undefined") { return this.captureWithDASH(video, streamUrl, { timeout, quality, mimeType, skipBlackFrame }); } break; } return this.captureNormal(video, streamUrl, { timeout, quality, mimeType, skipBlackFrame }); } finally { if (!existingVideo && video.parentNode) { video.parentNode.removeChild(video); } } } static async captureNormal(video, url, options) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); reject(new Error("StreamSnap timeout")); }, options.timeout); const cleanup = () => { clearTimeout(timer); video.removeEventListener("loadeddata", onLoadedData); video.removeEventListener("error", onError); video.src = ""; }; const onError = (e) => { cleanup(); reject(new Error(`Video error: ${e.message}`)); }; const onLoadedData = () => { try { const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not get canvas context"); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); if (!options.skipBlackFrame && isBlackFrame(ctx)) { throw new Error("Black frame detected"); } resolve(canvas.toDataURL(options.mimeType, options.quality)); } catch (err) { reject(err); } finally { cleanup(); } }; video.addEventListener("error", onError); video.addEventListener("loadeddata", onLoadedData); video.src = url; video.load(); }); } static async captureWithFLVJS(video, url, options) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); reject(new Error("FLVJS timeout")); }, options.timeout); const flvPlayer = window.flvjs.createPlayer({ type: "flv", url, isLive: true, cors: true }); flvPlayer.attachMediaElement(video); flvPlayer.load(); const cleanup = () => { clearTimeout(timer); video.removeEventListener("loadeddata", onLoadedData); video.removeEventListener("error", onError); flvPlayer.destroy(); }; const onError = (e) => { cleanup(); reject(new Error(`FLV error: ${e.message}`)); }; const onLoadedData = () => { try { const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not get canvas context"); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); if (!options.skipBlackFrame && isBlackFrame(ctx)) { throw new Error("Black frame detected"); } resolve(canvas.toDataURL(options.mimeType, options.quality)); } catch (err) { reject(err); } finally { cleanup(); } }; video.addEventListener("error", onError); video.addEventListener("loadeddata", onLoadedData); flvPlayer.play(); }); } static async captureWithHLS(video, url, options) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); reject(new Error("HLS timeout")); }, options.timeout); const hls = new window.Hls({ enableWorker: false, xhrSetup: (xhr) => { xhr.withCredentials = false; } }); hls.loadSource(url); hls.attachMedia(video); const cleanup = () => { clearTimeout(timer); video.removeEventListener("loadeddata", onLoadedData); video.removeEventListener("error", onError); hls.destroy(); }; const onError = (e) => { cleanup(); reject(new Error(`HLS error: ${e.message}`)); }; const onLoadedData = () => { try { const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not get canvas context"); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); if (!options.skipBlackFrame && isBlackFrame(ctx)) { throw new Error("Black frame detected"); } resolve(canvas.toDataURL(options.mimeType, options.quality)); } catch (err) { reject(err); } finally { cleanup(); } }; video.addEventListener("error", onError); video.addEventListener("loadeddata", onLoadedData); hls.on(window.Hls.Events.MANIFEST_PARSED, () => { video.play().catch(() => { video.muted = true; video.play(); }); }); }); } static async captureWithDASH(video, url, options) { return new Promise((resolve, reject) => { const timer = setTimeout(() => { cleanup(); reject(new Error("DASH timeout")); }, options.timeout); const player = window.dashjs.MediaPlayer().create(); player.extend( "RequestModifier", () => (request) => { request.withCredentials = false; return request; }, true // 第三个参数 override 设为 true ); player.initialize(video, url, false); const cleanup = () => { clearTimeout(timer); video.removeEventListener("loadeddata", onLoadedData); video.removeEventListener("error", onError); player.reset(); }; const onError = (e) => { cleanup(); reject(new Error(`DASH error: ${e.message}`)); }; const onLoadedData = () => { try { const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); if (!ctx) throw new Error("Could not get canvas context"); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); if (!options.skipBlackFrame && isBlackFrame(ctx)) { throw new Error("Black frame detected"); } resolve(canvas.toDataURL(options.mimeType, options.quality)); } catch (err) { reject(err); } finally { cleanup(); } }; video.addEventListener("error", onError); video.addEventListener("loadeddata", onLoadedData); player.play(); }); } }; var index_default = StreamSnap;