Skip to content

Commit

Permalink
videoPlayer refactor
Browse files Browse the repository at this point in the history
create player in shadow DOM; simplified transfer logic
  • Loading branch information
chadwallacehart committed Sep 22, 2024
1 parent e603eee commit cb18554
Show file tree
Hide file tree
Showing 11 changed files with 346 additions and 344 deletions.
8 changes: 4 additions & 4 deletions src/dash/dash.html
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
<div class="tab" data-target="#home">Home</div>
<div class="tab" data-target="#self-view">Self-view</div>
<div class="tab" data-target="#device-manager">Device Manager</div>
<div class="tab" data-target="#modify-stream">Modify Stream</div>
<div class="tab active" data-target="#modify-stream">Modify Stream</div>
<div class="tab" data-target="#record">Record</div>
<div class="tab active" data-target="#presence">Presence</div>
<div class="tab" data-target="#presence">Presence</div>
</div>
<div class="content" id="home">
<div class="d-flex h-100">
Expand Down Expand Up @@ -172,7 +172,7 @@ <h5>Hide Devices</h5>
<!-- END: DEVICE MANAGER -->

<!-- START: STREAM MODIFICATION -->
<div class="content" id="modify-stream">
<div class="content active" id="modify-stream">
<div class="d-flex h-100">
<button class="btn icon-button btn-outline-secondary streamModificationButton" data-bs-toggle="tooltip"
data-bs-placement="top" title="Enable Stream Modification">
Expand Down Expand Up @@ -257,7 +257,7 @@ <h5>Output</h5>
</div>
</div>

<div class="content active" id="presence">
<div class="content" id="presence">
<!-- Presence Control -->
<div class="d-flex flex-row flex-nowrap h-100">
<button class="btn icon-button btn-outline-secondary" id="presence-enable" data-bs-toggle="tooltip"
Expand Down
14 changes: 10 additions & 4 deletions src/extension-core/scripts/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,17 @@ window.vch = {
* Get the tabId from background
* - tabId is not directly exposed to content context
*/
mh.addListener(m.HELLO, (message)=>{
window.vch.tabId = message.tabId;
// debug("hello", message);
mh.addListener(m.TAB_ID, (message)=>{
const tabId = message.tabId;
if(!tabId){
debug("ERROR: tabId not provided on TAB_ID listener", message);
return;
}
debug(`this is tab ${tabId}`);
window.vch.tabId = tabId;
mh.sendMessage(c.INJECT, m.TAB_ID, {tabId: tabId});
});
mh.sendMessage(c.BACKGROUND, m.HELLO);
mh.sendMessage(c.BACKGROUND, m.TAB_ID);


/************ START inject script injection ************/
Expand Down
8 changes: 8 additions & 0 deletions src/extension-core/scripts/inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,13 @@ navigator.mediaDevices.removeEventListener = function (type, listener) {
return originalRemoveEventListener(type, listener);
};

// Get the tab id
mh.addListener(m.TAB_ID, message => {
vch.tabId = message.tabId;
debug(`this is tab ${message.tabId}`);
});

// ToDo: make this more general
// Settings updates that impact the inject context
mh.addListener(m.UPDATE_BAD_CONNECTION_SETTINGS, (data) => {
debug("UPDATE_BAD_CONNECTION_SETTINGS", data);
Expand All @@ -483,6 +490,7 @@ const vch = {
pcTracks: [],
gumTracks: [], // original, pre-shimmed getUserMedia tracks
pcs: [],
tabId: null
}

if (process.env.NODE_ENV) window.vch = vch;
Expand Down
3 changes: 1 addition & 2 deletions src/modules/messageHandler.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -653,7 +653,7 @@ export const CONTEXT = {
export const MESSAGE = {
PING: 'ping', // background -> content
PONG: 'pong', // content -> background
HELLO: 'hello', // content <-> background for getting tab
TAB_ID: 'tab_id', // content <-> background for getting tab

// used in inject.js
GET_ALL_SETTINGS: 'get_all_settings',
Expand Down Expand Up @@ -725,7 +725,6 @@ export const MESSAGE = {
// player
PLAYER_START: 'player_start',
PLAYER_LOAD: 'player_load',
PLAYER_TRANSFER: 'player_transfer',
PLAYER_PAUSE: 'player_pause',
PLAYER_RESUME: 'player_resume', // used in the worker to skip reading
PLAYER_END: 'player_end', // used in the worker to end the transform
Expand Down
Binary file added src/static/media/black_static.mp4
Binary file not shown.
24 changes: 0 additions & 24 deletions src/videoPlayer/scripts/background.mjs

This file was deleted.

220 changes: 38 additions & 182 deletions src/videoPlayer/scripts/content.mjs
Original file line number Diff line number Diff line change
@@ -1,201 +1,57 @@
import {ImageStream} from "../../imageCapture/scripts/content.mjs";
import {base64ToBuffer} from "./base64.mjs";

import {StorageHandler} from "../../modules/storageHandler.mjs";
import {MESSAGE as m, CONTEXT as c, MessageHandler} from "../../modules/messageHandler.mjs";

const debug = Function.prototype.bind.call(console.debug, console, `vch 🎥‍ `)
const storage = await new StorageHandler();
const mh = new MessageHandler(c.CONTENT);

/* type {HTMLVideoElement} */
let videoPlayer = null;
export let imagePreview = null; // used to hold the gUM preview image generator
let injectReady = false;


mh.addListener(m.INJECT_LOADED, async () => {
injectReady = true;
// Use a shadow root to isolate the video player from the page
const shadowContainer = document.createElement('div');
shadowContainer.id = 'vch-player-container';
const shadowRoot = shadowContainer.attachShadow({ mode: 'open' });

// create the video player
const videoPlayerElement = document.createElement('video');
const placeHolderVideo = chrome.runtime.getURL('media/black_static.mp4');
videoPlayerElement.src = placeHolderVideo;
videoPlayerElement.loop = true;
videoPlayerElement.id = `vch-player`;
videoPlayerElement.preload = "auto";
videoPlayerElement.hidden = true;
videoPlayerElement.muted = true;

// Append the video player element to the shadow root
shadowRoot.appendChild(videoPlayerElement);

// Append the shadow container to the body so inject can access it
document.addEventListener('DOMContentLoaded', async () => {
document.body.appendChild(shadowContainer);
debug("added video player element", videoPlayerElement);
});

// await storage.update('player', {active: false});


/*
Not easy to send a stream to dash:
* can't access the iframe content - CORS issues
* can't send the stream over postMessage - it's not serializable
* can't pass a resource URL - treated as different domains
ideas:
1. open a new stream - could cause gUM conflicts, more encoding
2. send snapshots - this is what did
*/
/**
* Grabs the last stream and generates preview thumbnails
* @returns {Promise<void>}
*/
export async function showPreview() {
const stream = window.vch.streams
.filter(stream=>stream.active)
.at(-1); // get the last stream // ToDo: get the highest res gUM stream
if (stream) {
imagePreview = new ImageStream(stream, 200, c.DASH, true);
debug("showPreview:: stream", stream);
await imagePreview.start();
} else
imagePreview = null;
}

// Stop the preview image stream if the dash is closed
/*
mh.addListener(m.TOGGLE_DASH, async () => {
// const iframe = document.querySelector('iframe#vch_dash');
// if (!iframe)
// wait for 500 ms to see if the dash is opening
await new Promise(resolve => setTimeout(resolve, 500));
debug(`toggleDash: dash is ${window.vch?.dashOpen ? "open" : "closed" }`);
if (window.vch.dashOpen)
await showPreview();
else if (imagePreview) {
imagePreview?.stop();
debug("showPreview stopped - dash closed");
} else
debug("showPreview not started and dash closed");
});
*/

// ToDo: consider if I want to show this
/*
// restart on a new stream
mh.addListener(m.GUM_STREAM_START, async () => {
if (window.vch.dashOpen) {
imagePreview?.stop();
debug("restarting showPreview on new gUM stream");
await showPreview();
}
});
*/

/**
* Load a media file into a video element
* @returns {Promise<void>}
*/
async function loadMedia() {

return new Promise(async (resolve, reject) => {

/**
* @param {string} buffer - base64 encoded video file
* @param {string} mimeType - the mime type of the video file
* @param {boolean} loop - loop the video (not used)
* @param {number} playbackOffset
*/

const { objectUrl, mimeType, loop, videoTimeOffsetMs, currentTime} = storage.contents['player'];
if(!objectUrl){
debug("no objectUrl in storage to load media");
resolve("no objectUrl in storage to load media");
return
}
if(!mimeType){
debug("no mimeType in storage to load media");
reject("no mimeType in storage to load media");
return
}

const transmissionDelay = new Date().getTime() - currentTime;
const playbackOffset = (videoTimeOffsetMs + transmissionDelay) / 1000;

// const arrayBuffer = base64ToBuffer(buffer);
// const blob = new Blob([arrayBuffer], {type: mimeType}); // Ensure the MIME type matches the video format

// Set up the video player if it doesn't exist
if(!videoPlayer) {
// ToDo: use shadow DOM
videoPlayer = document.createElement('video');
//videoPlayer.src = URL.createObjectURL(blob);
videoPlayer.src = objectUrl;
videoPlayer.loop = loop;
videoPlayer.id = `vch-player-${Math.random().toString().substring(2, 6)}`;
videoPlayer.preload = "auto";
videoPlayer.hidden = true;
videoPlayer.muted = true;
// set the style to fit to cover
// videoPlayer.style.cssText = "object-fit:cover;";

// captureStream takes the source video size so this doesn't matter
// const {width, height} = streams.at(-1)?.getVideoTracks()[0]?.getSettings();
// videoPlayer.height = height;
// videoPlayer.width = width;

document.body.appendChild(videoPlayer);
debug("added video element", videoPlayer);

function sendVideoElement(){
if(injectReady)
mh.sendMessage(c.INJECT, m.PLAYER_TRANSFER, {id: videoPlayer.id});
else
setTimeout(async ()=> sendVideoElement(), 500)
}
sendVideoElement();


videoPlayer.oncanplay = async () => {
videoPlayer.oncanplay = null;
// videoPlayer.currentTime = playbackOffset;
debug("Adjusted playback to match sync", playbackOffset);
resolve();
};
}
else {
// videoPlayer.src = URL.createObjectURL(blob);
videoPlayer.src = objectUrl;
debug("Set video source", objectUrl);
resolve();
}
});

}


/**
* Look for storage changes and update media if a new media file is loaded
* - used to preload media for faster playback
*/
storage.addListener('player', async (newValue) => {
debug("player storage changed", newValue);

// In the future also check for changes to other video params
if (newValue.currentTime) {
await loadMedia().catch(error => debug("Error loading media into player", error));
}
});


/**
* Tell inject to take the video player stream
*/
/*
mh.addListener(m.GUM_STREAM_START, async () => {
if (!storage.contents['player']?.enabled) return;
if (videoPlayer) {
mh.sendMessage(c.INJECT, m.PLAYER_START, { id: videoPlayer.id });
// await storage.update('player', { active: true });
} else {
try {
await loadMedia();
mh.sendMessage(c.INJECT, m.PLAYER_START, { id: videoPlayer.id });
// await storage.update('player', { active: true });
} catch (error) {
debug("Error loading media", error);
// debug("player storage changed", newValue);

if (newValue?.currentTime) {
// storage can be slow with large objects, so do await with get
const temp = await storage.get('temp');
const buffer = temp?.buffer;
// In the future also check for changes to other video params
if (buffer?.length) {
const arrayBuffer = base64ToBuffer(buffer);
// const mimeType = storage.contents['player']?.mimeType;
const blob = new Blob([arrayBuffer]); //, {type: mimeType}); // Ensure the MIME type matches the video format
videoPlayerElement.src = URL.createObjectURL(blob);
storage.delete('temp').catch(err => debug(err));
debug("loaded player media", buffer.length);
}
else
debug("failed load player media - invalid buffer", buffer);
}
});
*/

await loadMedia().catch(error => debug("Error loading media into player", error));
Loading

0 comments on commit cb18554

Please sign in to comment.