UNPKG

@eatsjobs/media-mock

Version:

Media-Mock is a JavaScript library that simulates media devices (like webcams) in web applications, allowing developers to test and debug media constraints, device configurations, and stream functionality without needing physical devices. This is particul

525 lines (524 loc) 17.6 kB
var w = Object.defineProperty; var k = (c, e, t) => e in c ? w(c, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : c[e] = t; var o = (c, e, t) => k(c, typeof e != "symbol" ? e + "" : e, t); function l(c, e, t) { const i = c[e]; return Object.defineProperty(c, e, { writable: !0, configurable: !0, value: t }), () => { Object.defineProperty(c, e, { writable: !0, configurable: !0, value: i }); }; } function m({ deviceId: c, groupId: e, kind: t, label: i, mockCapabilities: n = { width: { min: 1, max: 1280 }, height: { min: 1, max: 720 } } }) { return { deviceId: c, groupId: e, kind: t, label: i, getCapabilities: () => n, toJSON() { return { deviceId: `${this.deviceId}`, kind: this.kind, label: `${this.label}`, groupId: `${this.groupId}` }; } }; } const b = { aspectRatio: !0, deviceId: !0, displaySurface: !0, echoCancellation: !0, facingMode: !0, frameRate: !0, groupId: !1, height: !0, sampleRate: !1, sampleSize: !1, torch: !0, volume: !0, whiteBalanceMode: !0, width: !0, zoom: !0 }, B = { ...b, torch: !1 }, C = { "iPhone 12": { videoResolutions: [ { width: 1920, height: 1080 }, { width: 1280, height: 720 }, { width: 640, height: 480 }, { width: 320, height: 240 } ], mediaDeviceInfo: [ m({ deviceId: "A7FB77364106629BF38E043E6B000EE5FD680B9B", kind: "videoinput", label: "Front Camera", groupId: "C1B048C04520A18C3611DC837450814245482489", mockCapabilities: { aspectRatio: { max: 4032, min: 33068783068783067e-20 }, deviceId: "1A100C35A33042B643BE0438DBBF9FDC95AF1913", facingMode: ["user"], frameRate: { max: 60, min: 1 }, groupId: "C1B048C04520A18C3611DC837450814245482489", height: { max: 3024, min: 1 }, whiteBalanceMode: ["manual", "continuous"], width: { max: 4032, min: 1 }, zoom: { max: 4, min: 1 } } }), m({ deviceId: "9729B396E0C2B460BC7B69C0E368EB0B605058A9", kind: "videoinput", label: "Back Dual Wide Camera", groupId: "A1F2417053FF79495E7D01AF37A6C4461CE0C060", mockCapabilities: { aspectRatio: { max: 4032, min: 33068783068783067e-20 }, deviceId: "D87C414E22C375BB0697DCB83A24D97BD520624D", facingMode: ["environment"], focusDistance: { min: 0.12 }, frameRate: { max: 60, min: 1 }, groupId: "A1F2417053FF79495E7D01AF37A6C4461CE0C060", height: { max: 3024, min: 1 }, torch: !0, whiteBalanceMode: ["manual", "continuous"], width: { max: 4032, min: 1 }, zoom: { max: 2, min: 0.5 } } }), m({ deviceId: "0B74C1149038CA5235F6C2325E53AE22AA920379", kind: "videoinput", label: "Back Ultra Wide Camera", groupId: "B402A3862F28FB8D54BDF33BD7D41874FE175517", mockCapabilities: { aspectRatio: { max: 4032, min: 33068783068783067e-20 }, deviceId: "BE00A990BEDE2D324EB0AD51F567EE4ADC24D9B0", facingMode: ["environment"], focusDistance: { min: 0.12 }, frameRate: { max: 60, min: 1 }, groupId: "B402A3862F28FB8D54BDF33BD7D41874FE175517", height: { max: 3024, min: 1 }, torch: !0, whiteBalanceMode: ["manual", "continuous"], width: { max: 4032, min: 1 }, zoom: { max: 4, min: 1 } } }), m({ deviceId: "C92FE814FCB4F2F856CDCBFD1C555429774DD0E2", kind: "videoinput", label: "Back Camera", groupId: "14122C2CE97B69A84360822AB87E8206C32B5BD8", mockCapabilities: { aspectRatio: { max: 4032, min: 33068783068783067e-20 }, deviceId: "D13A012C1D5C9F9899B40BDA0790184EE57FD282", facingMode: ["environment"], focusDistance: { min: 0.12 }, frameRate: { max: 60, min: 1 }, groupId: "14122C2CE97B69A84360822AB87E8206C32B5BD8", height: { max: 3024, min: 1 }, torch: !0, whiteBalanceMode: ["manual", "continuous"], width: { max: 4032, min: 1 }, zoom: { max: 4, min: 1 } } }) ], supportedConstraints: b }, "Samsung Galaxy M53": { videoResolutions: [ { width: 1920, height: 1080 }, { width: 1280, height: 720 }, { width: 640, height: 480 } ], mediaDeviceInfo: [ m({ deviceId: "87fcafb209f5ff2a6d7c8a5d14afe1c9aba9f209330e93933e545e40b102b35f", groupId: "f70f63d2f4eea57dafe6c6b60833aa69a02f06bb0a6878cb277fb4d70daa9020", kind: "videoinput", label: "camera2 1, facing front", mockCapabilities: { aspectRatio: { max: 2400, min: 9191176470588235e-19 }, deviceId: "87fcafb209f5ff2a6d7c8a5d14afe1c9aba9f209330e93933e545e40b102b35f", facingMode: ["user"], frameRate: { max: 30, min: 1 }, groupId: "f70f63d2f4eea57dafe6c6b60833aa69a02f06bb0a6878cb277fb4d70daa9020", height: { max: 1088, min: 1 }, resizeMode: ["none", "crop-and-scale"], width: { max: 2400, min: 1 } } }), m({ deviceId: "81cb5898aebd672ef65d04ed1bc7b00c704f2b6aa94200bc5556ff02c89ea14d", groupId: "7300f91d6cb037dcaa6fe16abb59f4e9f92fb471e2280ff0e313e07c49cb536c", kind: "videoinput", label: "camera2 2, facing front", mockCapabilities: { aspectRatio: { max: 2400, min: 9191176470588235e-19 }, deviceId: "81cb5898aebd672ef65d04ed1bc7b00c704f2b6aa94200bc5556ff02c89ea14d", facingMode: ["user"], frameRate: { max: 30, min: 1 }, groupId: "7300f91d6cb037dcaa6fe16abb59f4e9f92fb471e2280ff0e313e07c49cb536c", height: { max: 1088, min: 1 }, resizeMode: ["none", "crop-and-scale"], width: { max: 2400, min: 1 } } }), m({ deviceId: "99be6eecad8c050052df5dbb08b0460d2715b0a3b18fc5c7f08d6073d312ca34", groupId: "40f44b864c99ab042a21cf87df882d0ef5c7f88f7cbfcee74cefc1e393b8616b", kind: "videoinput", label: "camera2 0, facing back", mockCapabilities: { aspectRatio: { max: 3840, min: 462962962962963e-18 }, deviceId: "99be6eecad8c050052df5dbb08b0460d2715b0a3b18fc5c7f08d6073d312ca34", facingMode: ["environment"], frameRate: { max: 30, min: 1 }, groupId: "40f44b864c99ab042a21cf87df882d0ef5c7f88f7cbfcee74cefc1e393b8616b", height: { max: 2160, min: 1 }, resizeMode: ["none", "crop-and-scale"], width: { max: 3840, min: 1 }, torch: !0 } }) ], supportedConstraints: b }, "Mac Desktop": { videoResolutions: [ { width: 1920, height: 1080 }, { width: 1280, height: 720 }, { width: 640, height: 480 } ], mediaDeviceInfo: [ m({ deviceId: "e91a0ba82ba051029709163c442d340a3919dfabd", groupId: "7ce19c839ef9ab1a4cba8d4dd4d3c1bbbf3ad", kind: "videoinput", label: "FaceTime HD Camera (2C0E:82E3)", mockCapabilities: { aspectRatio: { max: 1920, min: 5208333333333333e-19 }, backgroundBlur: [!1], deviceId: "370CF6B3449B7B73599E8DAEEE75FB41788A0712", frameRate: { max: 30, min: 1 }, groupId: "F2EFF7249C97B5531FF959C8F977138341165F6B", height: { max: 1920, min: 1 }, width: { max: 1920, min: 1 } } }) ], supportedConstraints: B } }; async function x(c) { const e = new Image(); e.src = c; try { return await e.decode(), e; } catch { throw new Error(`Failed to load image: ${c}`); } } function F() { return { mediaDevices: { getUserMedia: !0, getSupportedConstraints: !0, enumerateDevices: !0 } }; } function M(c) { var i; const e = [ "mp4", "webm", "ogg", "mov", "avi", "mkv", "flv", "wmv", "m4v", "3gp", "mpg", "mpeg", "asf", "rm", "vob" ], t = (i = c.split(".").pop()) == null ? void 0 : i.toLowerCase(); return e.includes(t ?? ""); } function E(c) { return new Promise((e) => { c.addEventListener("loadeddata", async () => { try { await c.play(), e(); } catch (t) { console.error(t), e(); } }), c.load(); }); } class R { constructor() { o(this, "settings", { mediaURL: "./assets/640x480-sample.png", device: C["iPhone 12"], constraints: C["iPhone 12"].supportedConstraints, canvasScaleFactor: 1 }); o(this, "mediaMockImageId", "media-mock-image"); o(this, "mediaMockCanvasId", "media-mock-canvas"); o(this, "currentImage"); o(this, "mapUnmockFunction", /* @__PURE__ */ new Map()); o(this, "currentStream"); o(this, "intervalId", null); o(this, "debug", !1); o(this, "canvas"); o(this, "ctx"); o(this, "mockedVideoTracksHandler", (e) => e); o(this, "fps", 30); o(this, "resolution", { width: 640, height: 480 }); } /** * The Image or the video that will be used as source. * @public * @param {string} url * @returns {typeof MediaMock} */ setMediaURL(e) { return this.settings.mediaURL = e, this.intervalId && this.startIntervalDrawing(), this; } async startIntervalDrawing() { var i, n; this.intervalId && clearInterval(this.intervalId); const { width: e, height: t } = this.resolution; if (M(this.settings.mediaURL)) { const d = document.createElement("video"); d.addEventListener("error", () => { console.error( "Failed to load video source. Ensure the format is supported and the URL is valid." ); }, { once: !0 }), d.src = this.settings.mediaURL, d.muted = !0, d.playsInline = !0, d.loop = !0, d.autoplay = !0, d.hidden = !0, d.crossOrigin = "anonymous", await E(d), this.intervalId = setInterval(() => { var s, a, r; (s = this.ctx) == null || s.clearRect(0, 0, e, t), this.ctx.fillStyle = "#ffffff", (a = this.ctx) == null || a.fillRect(0, 0, e, t), (r = this.ctx) == null || r.drawImage(d, 0, 0, e, t); }, 1e3 / this.fps); } else this.currentImage = await x(this.settings.mediaURL), this.currentImage.id = this.mediaMockImageId, this.debug && console.log( ` Canvas: ${e}x${t}, Image: ${(i = this.currentImage) == null ? void 0 : i.naturalWidth}x${(n = this.currentImage) == null ? void 0 : n.naturalHeight}` ), this.intervalId = setInterval(() => { var p, I, D; (p = this.ctx) == null || p.clearRect(0, 0, e, t), this.ctx.fillStyle = "#ffffff", (I = this.ctx) == null || I.fillRect(0, 0, e, t); const { naturalWidth: d, naturalHeight: s } = this.currentImage, a = d / s, r = e / t; let h, u, g, v; const f = this.settings.canvasScaleFactor; a > r ? (h = e * f, u = e * f / a, g = (e - h) / 2, v = (t - u) / 2) : (u = t * f, h = t * f * a, g = (e - h) / 2, v = (t - u) / 2), (D = this.ctx) == null || D.drawImage( this.currentImage, g, v, h, u ); }, 1e3 / this.fps); } /** * Add a new device and trigger a device change event. * * @public * @param {MockMediaDeviceInfo} newDevice */ addMockDevice(e) { return this.settings.device.mediaDeviceInfo.push(e), this.triggerDeviceChange(), this; } /** * Remove a device and trigger a device change event. * * @public * @param {string} deviceId */ removeMockDevice(e) { return this.settings.device.mediaDeviceInfo = this.settings.device.mediaDeviceInfo.filter( (t) => t.deviceId !== e ), this.triggerDeviceChange(), this; } triggerDeviceChange() { navigator.mediaDevices.dispatchEvent(new Event("devicechange")); } /** * Debug mode will append the canvas and loaded image to the body if available. * * @public */ enableDebugMode() { return this.debug = !0, this.canvas != null && document.querySelector(this.mediaMockCanvasId) == null && (this.canvas.style.border = "10px solid red", document.body.append(this.canvas)), this.currentImage != null && document.querySelector(this.mediaMockImageId) == null && (this.currentImage.style.border = "10px solid red", document.body.append(this.currentImage)), this; } /** * Removes the debug canvas and image from the body. * * @public * @returns {typeof MediaMock} */ disableDebugMode() { this.debug = !1; const e = document.getElementById(this.mediaMockCanvasId), t = document.getElementById(this.mediaMockImageId); return e == null || e.remove(), t == null || t.remove(), this; } setMockedVideoTracksHandler(e) { return this.mockedVideoTracksHandler = e, this; } /** * Replaces the navigator.mediaDevices functions. * * @public * @param {DeviceConfig} device * @param {MockOptions} [options=createDefaultMockOptions()] * @returns {typeof MediaMock} */ mock(e, t = F()) { if (this.settings.device = e, typeof navigator.mediaDevices > "u") { class i extends EventTarget { } l(navigator, "mediaDevices", new i()); } if (t != null && t.mediaDevices.getUserMedia) { const i = l( navigator.mediaDevices, "getUserMedia", (n) => this.getMockStream(n) ); this.mapUnmockFunction.set("getUserMedia", i); } if (t != null && t.mediaDevices.getSupportedConstraints) { const i = l( navigator.mediaDevices, "getSupportedConstraints", () => this.settings.constraints ); this.mapUnmockFunction.set( "getSupportedConstraints", i ); } if (t != null && t.mediaDevices.enumerateDevices) { const i = l( navigator.mediaDevices, "enumerateDevices", async () => this.settings.device.mediaDeviceInfo ); this.mapUnmockFunction.set("enumerateDevices", i); } return this; } /** * Stops the mock and removes the mock functions. * * @public * @returns {typeof MediaMock} */ unmock() { return this.stopMockStream(), this.disableDebugMode(), this.mapUnmockFunction.forEach((e) => e()), this.mapUnmockFunction.clear(), this; } stopMockStream() { var e, t, i, n; this.intervalId && clearInterval(this.intervalId), (t = (e = this.currentStream) == null ? void 0 : e.getVideoTracks()) == null || t.forEach((d) => d.stop()), (n = (i = this.currentStream) == null ? void 0 : i.stop) == null || n.call(i); } /** * Set the scale factor for the image in the canvas. * Values between 0 and N, where lower values create more margin, * and higher values fill more of the canvas. * * @public * @param {number} factor - Scale factor between 0 and N * @returns {typeof MediaMock} */ setCanvasScaleFactor(e) { return this.settings.canvasScaleFactor = Math.max(0.1, e), this; } async getMockStream(e) { this.resolution = this.getResolution( e, this.settings.device ), this.fps = this.getFPSFromConstraints(e), this.canvas = document.createElement("canvas"), this.canvas.id = this.mediaMockCanvasId; const { width: t, height: i } = this.resolution; this.canvas.width = t, this.canvas.height = i, this.ctx = this.canvas.getContext("2d"), this.ctx.fillStyle = "#ffffff", this.ctx.fillRect(0, 0, t, i), await this.startIntervalDrawing(), this.debug && this.enableDebugMode(); const n = this.canvas.captureStream(this.fps); return this.currentStream = new MediaStream( this.mockedVideoTracksHandler( (n == null ? void 0 : n.getVideoTracks()) ?? [] ) ), this.currentStream; } getFPSFromConstraints(e) { return typeof e.video == "object" && e.video.frameRate ? typeof e.video.frameRate == "number" ? e.video.frameRate : e.video.frameRate.ideal || 30 : 30; } /** * Get the appropriate resolution based on device orientation * @param constraints Media constraints * @param deviceConfig Device configuration * @returns Resolution object with width and height */ getResolution(e, t) { const i = window.innerHeight > window.innerWidth, n = e.video.width || { ideal: 640 }, d = e.video.height || { ideal: 480 }; let s; if (i) { if (s = t.videoResolutions.find( (a) => a.height > a.width && // Find portrait-oriented resolutions first (typeof d == "number" ? a.height === d : a.height === d.ideal) && (typeof n == "number" ? a.width === n : a.width === n.ideal) ), !s) { const a = t.videoResolutions.find( (r) => (typeof n == "number" ? r.height === n : r.height === n.ideal) && (typeof d == "number" ? r.width === d : r.width === d.ideal) ); a && (s = { width: a.height, height: a.width }); } } else s = t.videoResolutions.find( (a) => (typeof n == "number" ? a.width === n : a.width === n.ideal) && (typeof d == "number" ? a.height === d : a.height === d.ideal) ); if (!s && i && (s = t.videoResolutions.find((a) => a.height > a.width), !s && t.videoResolutions.length > 0)) { const a = t.videoResolutions[0]; s = { width: a.height, height: a.width }; } return s || t.videoResolutions[0]; } } const A = new R(); export { A as MediaMock, R as MediaMockClass, m as createMediaDeviceInfo, C as devices };