Last active
February 23, 2023 13:27
-
-
Save recursivecodes/85e7073dd1438e572d82bc1b44d5f331 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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(); | |
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* 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