Skip to content

Instantly share code, notes, and snippets.

@recursivecodes
Last active February 23, 2023 13:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save recursivecodes/85e7073dd1438e572d82bc1b44d5f331 to your computer and use it in GitHub Desktop.
Save recursivecodes/85e7073dd1438e572d82bc1b44d5f331 to your computer and use it in GitHub Desktop.
<div class="row">
<div class="col-lg-2 offset-lg-2">
<div class="h-100 border rounded shadow p-3">
<img id="webcam-view-btn" src="/images/camera_icon.png" class="ratio ratio-16x9 border border-5 border-secondary rounded img-responsive shadow mb-2" role="button" />
<video id="vod-0" src="/video/vod-0.mp4" class="border border-5 border-secondary ratio ratio-16x9 rounded shadow" controls></video>
<video id="vod-1" src="/video/vod-1.mp4" class="border border-5 border-secondary ratio ratio-16x9 rounded shadow" controls></video>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow">
<div class="position-relative">
<div class="ratio ratio-16x9">
<canvas id="broadcast-preview" class="rounded-top"></canvas>
</div>
<div class="position-absolute top-0 end-0 p-2">
<span class="badge bg-white text-dark" id="online-indicator">Offline</span>
</div>
</div>
<div class="card-footer bg-white d-flex justify-content-center flex-column flex-lg-row">
<div class="w-100 me-3">
<select name="camera-select" id="camera-select" class="h-100 form-select me-lg-3 mb-2 mb-lg-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Select Video Source"></select>
</div>
<div class="w-100 me-3">
<select name="mic-select" id="mic-select" class="h-100 form-select me-lg-3 mb-2 mb-lg-0" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Select Audio Source"></select>
</div>
<div class="flex-fill mb-2 mb-lg-0">
<button id="broadcast-btn" class="btn btn-outline-success text-nowrap h-100 w-100" data-bs-toggle="tooltip" data-bs-placement="bottom" title="Toggle Broadcast">Broadcast</button>
</div>
</div>
</div>
</div>
</div>
{{#> page/footer}}
{{/ page/footer}}
<script src="./ivs-web-broadcast-vod.js" type="module"></script>
<script src="https://web-broadcast.live-video.net/1.2.0/amazon-ivs-web-broadcast.js"></script>
/* globals IVSBroadcastClient bootstrap */
import { Utils } from './util.js';
const utils = new Utils();
window.isBroadcasting = false;
window.isVodPlaying = false;
window.isSettingCameraView = false;
const handlePermissions = async () => {
let permissions;
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
for (const track of stream.getTracks()) {
track.stop();
}
permissions = { video: true, audio: true };
}
catch (err) {
permissions = { video: false, audio: false };
console.error(err.message);
}
if (!permissions.video) {
console.error('Failed to get video permissions.');
} else if (!permissions.audio) {
console.error('Failed to get audio permissions.');
}
};
const init = async () => {
await handlePermissions();
await getMediaDevices();
window.broadcastClient = IVSBroadcastClient.create({
streamConfig: IVSBroadcastClient.STANDARD_LANDSCAPE,
ingestEndpoint: window.ingestEndpoint,
});
await createWebcamStream();
await createMicStream();
previewVideo();
document.getElementById('broadcast-btn').addEventListener('click', toggleBroadcast);
document.getElementById('camera-select').addEventListener('change', selectCamera);
document.getElementById('mic-select').addEventListener('change', selectMic);
document.getElementById('webcam-view-btn').addEventListener('click', async () => {
window.isSettingCameraView = true;
await cameraView();
window.isSettingCameraView = false;
});
initVodView();
initVodWebcamView();
};
const getMediaDevices = async () => {
const cameraSelect = document.getElementById('camera-select');
const micSelect = document.getElementById('mic-select');
const devices = await navigator.mediaDevices.enumerateDevices();
window.videoDevices = devices.filter((d) => d.kind === 'videoinput');
window.audioDevices = devices.filter((d) => d.kind === 'audioinput');
window.videoDevices.forEach((device, idx) => {
const opt = document.createElement('option');
opt.value = device.deviceId;
opt.innerHTML = device.label;
if (idx === 0) {
window.selectedVideoDeviceId = device.deviceId;
opt.selected = true;
}
cameraSelect.appendChild(opt);
});
window.audioDevices.forEach((device, idx) => {
const opt = document.createElement('option');
opt.value = device.deviceId;
opt.innerHTML = device.label;
if (idx === 0) {
window.selectedAudioDeviceId = device.deviceId;
opt.selected = true;
}
micSelect.appendChild(opt);
});
};
const resetViews = async () => {
const client = window.broadcastClient;
if (!client) return;
const cameraExists = client.getVideoInputDevice('camera-0');
if (cameraExists) await client.removeVideoInputDevice('camera-0');
const pipCameraExists = client.getVideoInputDevice('pip-camera-0');
if (pipCameraExists) await client.removeVideoInputDevice('pip-camera-0');
const vod1Exists = client.getVideoInputDevice('vod-0');
if (vod1Exists) await client.removeImage('vod-0');
const vod2Exists = client.getVideoInputDevice('vod-1');
if (vod2Exists) await client.removeImage('vod-1');
const vod1AudioExists = window.broadcastClient.getAudioInputDevice('vod-0-audio');
if (vod1AudioExists) window.broadcastClient.removeAudioInputDevice('vod-0-audio');
const vod2AudioExists = window.broadcastClient.getAudioInputDevice('vod-1-audio');
if (vod2AudioExists) window.broadcastClient.removeAudioInputDevice('vod-1-audio');
};
const getVideoStream = async () => {
const streamConfig = IVSBroadcastClient.STANDARD_LANDSCAPE;
const videoStream = await navigator.mediaDevices.getUserMedia({
video: {
deviceId: {
exact: window.selectedVideoDeviceId
},
width: {
ideal: streamConfig.maxResolution.width,
max: streamConfig.maxResolution.width,
},
height: {
ideal: streamConfig.maxResolution.height,
max: streamConfig.maxResolution.height,
},
},
});
return videoStream;
};
/*
VOD Webcam View ("react video" format):
* stream a pre-recorded VOD
* stream audio from VOD
* overlay webcam (PIP)
* include mic audio
*/
const initVodWebcamView = async (evt) => {
const vod1 = document.getElementById('vod-1');
document.getElementById('vod-0').pause();
vod1.addEventListener('play', async (evt) => {
// remove all cameras/vods/audio
await resetViews();
// add VOD 2 to broadcast
window.broadcastClient.addImageSource(evt.target, 'vod-1', { index: 0 });
// add camera as PIP
const streamConfig = IVSBroadcastClient.STANDARD_LANDSCAPE;
const videoStream = await getVideoStream();
const preview = document.getElementById('broadcast-preview');
window.broadcastClient.addVideoInputDevice(videoStream, 'pip-camera-0', {
index: 1,
height: streamConfig.maxResolution.height * .25,
width: streamConfig.maxResolution.width * .25,
x: preview.width - preview.width / 4 - 20,
y: preview.height - preview.height / 4 - 20
});
// add audio from VOD
window.broadcastClient.addAudioInputDevice(vod1.captureStream(), 'vod-0-audio');
});
// listen for end of VOD
vod1.addEventListener('ended', cameraView);
};
/*
VOD View:
* stream a pre-recorded VOD
* stream audio from VOD
* no webcam
* no mic
*/
const initVodView = async () => {
const vod0 = document.getElementById('vod-0');
document.getElementById('vod-1').pause();
vod0.addEventListener('play', async (evt) => {
if (!window.broadcastClient) return;
await resetViews();
// remove mic
const micExists = window.broadcastClient.getAudioInputDevice('mic-0');
if (micExists) await window.broadcastClient.removeAudioInputDevice('mic-0');
// add VOD to broadcast
window.broadcastClient.addImageSource(evt.target, 'vod-0', { index: 0 });
// add audio from VOD
window.broadcastClient.addAudioInputDevice(vod0.captureStream(), 'vod-0-audio');
});
// listen for end of VOD
vod0.addEventListener('ended', cameraView);
vod0.addEventListener('pause', async (evt) => {
if (!window.isSettingCameraView && !evt.target.ended) {
await cameraView()
}
});
};
const cameraView = async () => {
await resetViews();
const vod0 = document.getElementById('vod-0');
if (!vod0.paused) vod0.pause();
const vod1 = document.getElementById('vod-1');
if (!vod1.paused) vod1.pause();
await createMicStream();
await createWebcamStream();
};
const previewVideo = () => {
const previewEl = document.getElementById('broadcast-preview');
window.broadcastClient.attachPreview(previewEl);
};
const selectCamera = async (e) => {
window.selectedVideoDeviceId = e.target.value;
await createWebcamStream();
};
const selectMic = async (e) => {
window.selectedAudioDeviceId = e.target.value;
await createMicStream();
};
const createWebcamStream = async () => {
if (!window.broadcastClient) return;
if (window.broadcastClient.getVideoInputDevice('camera-0')) return;
const streamConfig = IVSBroadcastClient.STANDARD_LANDSCAPE;
window.videoStream = await getVideoStream();
await window.broadcastClient.addVideoInputDevice(window.videoStream, 'camera-0', { index: 0 });
};
const createMicStream = async () => {
if (!window.broadcastClient) return;
console.log(window.broadcastClient.getAudioInputDevice('mic-0'))
if (window.broadcastClient.getAudioInputDevice('mic-0')) return;
window.audioStream = await navigator.mediaDevices.getUserMedia({ audio: { deviceId: window.selectedAudioDeviceId } });
await window.broadcastClient.addAudioInputDevice(window.audioStream, 'mic-0');
};
const startBroadcast = () => {
window.broadcastClient
.startBroadcast(window.streamKey)
.then(() => {
window.isBroadcasting = true;
toggleBtnsIndicators();
})
.catch((error) => {
window.isBroadcasting = false;
console.error(error);
});
};
const stopBroadcast = async () => {
window.broadcastClient.stopBroadcast();
window.isBroadcasting = false;
await createWebcamStream();
toggleBtnsIndicators();
};
const toggleBroadcast = async () => {
if (!window.isBroadcasting) {
startBroadcast();
}
else {
stopBroadcast();
}
};
const toggleBtnsIndicators = () => {
const broadcastBtn = document.getElementById('broadcast-btn');
if (window.isBroadcasting) {
broadcastBtn.classList.remove('btn-outline-success');
broadcastBtn.classList.add('btn-danger');
broadcastBtn.innerHTML = 'Stop Broadcast';
}
else {
broadcastBtn.classList.add('btn-outline-success');
broadcastBtn.classList.remove('btn-danger');
broadcastBtn.innerHTML = 'Broadcast';
}
utils.toggleIndicators(window.isBroadcasting);
};
document.addEventListener('DOMContentLoaded', async () => {
const streamConfig = await utils.getStreamConfig();
window.streamKey = streamConfig.streamKey;
window.ingestEndpoint = streamConfig.endpoint;
await init();
});
/* eslint-disable no-unused-vars */
export class Utils {
async getStreamUrl() {
const req = await fetch('/stream-url');
const j = await req.json();
return j.streamUrl;
}
async getStreamConfig() {
const req = await fetch('/stream-config');
const config = await req.json();
return config;
}
toggleIndicators(playing) {
const indicator = document.getElementById('online-indicator');
if (playing) {
indicator.classList.remove('bg-white');
indicator.classList.remove('text-dark');
indicator.classList.add('bg-danger');
indicator.classList.add('text-white');
indicator.innerHTML = 'LIVE';
}
else {
indicator.classList.remove('bg-danger');
indicator.classList.remove('text-white');
indicator.classList.add('bg-white');
indicator.classList.add('text-dark');
indicator.innerHTML = 'Offline';
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment