[mirotalk] - #152 fix and performance improvements

This commit is contained in:
Miroslav Pejic
2023-10-11 13:34:09 +02:00
parent 385637d65f
commit ad3168b852
3 changed files with 172 additions and 152 deletions
+1 -1
View File
@@ -38,7 +38,7 @@ dependencies: {
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.0.9
* @version 1.1.0
*
*/
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "mirotalk",
"version": "1.0.9",
"version": "1.1.0",
"description": "A free WebRTC browser-based video call",
"main": "server.js",
"scripts": {
+170 -150
View File
@@ -15,7 +15,7 @@
* @license For commercial use or closed source, contact us at license.mirotalk@gmail.com or purchase directly from CodeCanyon
* @license CodeCanyon: https://codecanyon.net/item/mirotalk-p2p-webrtc-realtime-video-conferences/38376661
* @author Miroslav Pejic - miroslav.pejic.85@gmail.com
* @version 1.0.9
* @version 1.1.0
*
*/
@@ -1373,14 +1373,14 @@ async function whoAreYou() {
videoSelect.selectedIndex = initVideoSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
myVideoChange = true;
await refreshLocalMedia();
await changeInitCamera(initVideoSelect.value);
await handleLocalCameraMirror();
};
initMicrophoneSelect.onchange = async () => {
audioInputSelect.selectedIndex = initMicrophoneSelect.selectedIndex;
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
myVideoChange = false;
await refreshLocalMedia();
await changeLocalMicrophone(audioInputSelect.value);
};
initSpeakerSelect.onchange = () => {
audioOutputSelect.selectedIndex = initSpeakerSelect.selectedIndex;
@@ -1496,8 +1496,8 @@ async function loadLocalStorage() {
// Start init cam
if (useVideo && initVideoSelect.value) {
myVideoChange = true;
await refreshLocalMedia();
await changeInitCamera(initVideoSelect.value);
await handleLocalCameraMirror();
}
}
@@ -1534,9 +1534,14 @@ async function changeInitCamera(deviceId) {
navigator.mediaDevices
.getUserMedia({ video: videoConstraints })
.then((camStream) => {
// We going to update init video stream
initVideo.srcObject = camStream;
initStream = camStream;
console.log('Success attached init video stream', initStream.getVideoTracks()[0].getSettings());
// We going to update also the local video
myVideo.srcObject = camStream;
localVideoMediaStream = camStream;
console.log('Success attached local video stream', localVideoMediaStream.getVideoTracks()[0].getSettings());
checkInitConfig();
})
.catch((err) => {
@@ -1545,6 +1550,68 @@ async function changeInitCamera(deviceId) {
});
}
/**
* Change local camera by device id
* @param {string} deviceId
*/
async function changeLocalCamera(deviceId) {
if (localVideoMediaStream) {
await stopVideoTracks(localVideoMediaStream);
myVideo.style.display = 'block';
if (!myVideo.classList.contains('mirror')) {
myVideo.classList.toggle('mirror');
}
}
// Get video constraints
let videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default');
videoConstraints['deviceId'] = { exact: deviceId };
navigator.mediaDevices
.getUserMedia({ video: videoConstraints })
.then((camStream) => {
myVideo.srcObject = camStream;
localVideoMediaStream = camStream;
console.log('Success attached local video stream', localVideoMediaStream.getVideoTracks()[0].getSettings());
refreshMyStreamToPeers(camStream);
})
.catch((err) => {
console.error('[Error] changeLocalCamera', err);
userLog('error', 'Error while swapping local camera' + err);
});
}
/**
* Change local microphone by device id
* @param {string} deviceId
*/
async function changeLocalMicrophone(deviceId) {
if (localAudioMediaStream) {
await stopAudioTracks(localAudioMediaStream);
}
// Get audio constraints
let audioConstraints = await getAudioConstraints();
audioConstraints['deviceId'] = { exact: deviceId };
navigator.mediaDevices
.getUserMedia({ audio: audioConstraints })
.then((micStream) => {
myAudio.srcObject = micStream;
localAudioMediaStream = micStream;
console.log(
'Success attached local microphone stream',
localAudioMediaStream.getAudioTracks()[0].getSettings(),
);
getMicrophoneVolumeIndicator(micStream);
refreshMyStreamToPeers(micStream, true);
})
.catch((err) => {
console.error('[Error] changeLocalMicrophone', err);
userLog('error', 'Error while swapping local microphone' + err);
});
}
/**
* Check peer audio and video &audio=1&video=1
* 1/true = enabled / 0/false = disabled
@@ -1573,7 +1640,6 @@ function checkPeerAudioVideo() {
* Room and Peer name are ok Join Channel
*/
async function whoAreYouJoin() {
if (isMobileDevice && myVideoStatus && myAudioStatus) await refreshLocalMedia();
myVideoWrap.style.display = 'inline';
myVideoParagraph.innerText = myPeerName + ' (me)';
setPeerAvatarImgName('myVideoAvatarImage', myPeerName);
@@ -2295,7 +2361,7 @@ function enumerateVideoDevices(stream) {
* Stop tracks from stream
* @param {object} stream
*/
function stopTracks(stream) {
async function stopTracks(stream) {
stream.getTracks().forEach((track) => {
track.stop();
});
@@ -2346,6 +2412,7 @@ async function setupLocalVideoMedia() {
try {
const stream = await navigator.mediaDevices.getUserMedia({ video: videoConstraints });
if (stream) {
localVideoMediaStream = stream;
await loadLocalMedia(stream, 'video');
console.log('10. Access granted to video device');
}
@@ -2373,6 +2440,7 @@ async function setupLocalAudioMedia() {
if (stream) {
await loadLocalMedia(stream, 'audio');
if (useAudio) {
localAudioMediaStream = stream;
await getMicrophoneVolumeIndicator(stream);
console.log('10. Access granted to audio device');
}
@@ -2403,8 +2471,6 @@ async function loadLocalMedia(stream, kind) {
//alert('local video');
console.log('SETUP LOCAL VIDEO STREAM');
localVideoMediaStream = stream;
// local video elements
const myVideoWrap = document.createElement('div');
const myLocalMedia = document.createElement('video');
@@ -2560,8 +2626,8 @@ async function loadLocalMedia(stream, kind) {
getId('videoMediaContainer').appendChild(myVideoWrap);
myVideoWrap.style.display = 'none';
logStreamSettingsInfo('localVideoMediaStream', localVideoMediaStream);
attachMediaStream(myLocalMedia, localVideoMediaStream);
logStreamSettingsInfo('localVideoMediaStream', stream);
attachMediaStream(myLocalMedia, stream);
adaptAspectRatio();
if (isVideoFullScreenSupported) {
@@ -2585,7 +2651,7 @@ async function loadLocalMedia(stream, kind) {
handleVideoZoomInOut(myVideoZoomInBtn.id, myVideoZoomOutBtn.id, myLocalMedia.id);
refreshMyVideoStatus(localVideoMediaStream);
refreshMyVideoStatus(stream);
if (!useVideo) {
const videoBtn = getId('videoBtn');
@@ -2609,7 +2675,6 @@ async function loadLocalMedia(stream, kind) {
case 'audio':
//alert('local audio');
console.log('SETUP LOCAL AUDIO STREAM');
localAudioMediaStream = stream;
// handle remote audio elements
const audioMediaContainer = getId('audioMediaContainer');
const localAudioWrap = document.createElement('div');
@@ -2621,9 +2686,9 @@ async function loadLocalMedia(stream, kind) {
localAudioMedia.volume = 0;
localAudioWrap.appendChild(localAudioMedia);
audioMediaContainer.appendChild(localAudioWrap);
logStreamSettingsInfo('localAudioMediaStream', localAudioMediaStream);
attachMediaStream(localAudioMedia, localAudioMediaStream);
refreshMyAudioStatus(localAudioMediaStream);
logStreamSettingsInfo('localAudioMediaStream', stream);
attachMediaStream(localAudioMedia, stream);
refreshMyAudioStatus(stream);
break;
default:
break;
@@ -3715,8 +3780,8 @@ function setAudioBtn() {
* Video hide - show button click event
*/
function setVideoBtn() {
videoBtn.addEventListener('click', (e) => {
handleVideo(e, false);
videoBtn.addEventListener('click', async (e) => {
await handleVideo(e, false);
});
}
@@ -4384,7 +4449,7 @@ function setupMySettings() {
// select audio input
audioInputSelect.addEventListener('change', async () => {
myVideoChange = false;
await refreshLocalMedia();
await changeLocalMicrophone(audioInputSelect.value);
lS.setLocalStorageDevices(lS.MEDIA_TYPE.audio, audioInputSelect.selectedIndex, audioInputSelect.value);
});
// select audio output
@@ -4395,7 +4460,8 @@ function setupMySettings() {
// select video input
videoSelect.addEventListener('change', async () => {
myVideoChange = true;
await refreshLocalMedia();
await changeLocalCamera(videoSelect.value);
await handleLocalCameraMirror();
lS.setLocalStorageDevices(lS.MEDIA_TYPE.video, videoSelect.selectedIndex, videoSelect.value);
});
// select video quality
@@ -4408,7 +4474,7 @@ function setupMySettings() {
} else {
// select video fps
videoFpsSelect.addEventListener('change', (e) => {
videoMaxFrameRate = parseInt(videoFpsSelect.value);
videoMaxFrameRate = parseInt(videoFpsSelect.value, 10);
setLocalMaxFps(videoMaxFrameRate);
lsSettings.video_fps = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
@@ -4416,7 +4482,7 @@ function setupMySettings() {
}
// select screen fps
screenFpsSelect.addEventListener('change', (e) => {
screenMaxFrameRate = parseInt(screenFpsSelect.value);
screenMaxFrameRate = parseInt(screenFpsSelect.value, 10);
if (isScreenStreaming) setLocalMaxFps(screenMaxFrameRate);
lsSettings.screen_fps = e.currentTarget.selectedIndex;
lS.setSettings(lsSettings);
@@ -4489,8 +4555,8 @@ function loadSettingsFromLocalStorage() {
msgerSpeechMsg.checked = speechInMessages;
screenFpsSelect.selectedIndex = lsSettings.screen_fps;
videoFpsSelect.selectedIndex = lsSettings.video_fps;
screenMaxFrameRate = parseInt(getSelectedIndexValue(screenFpsSelect));
videoMaxFrameRate = parseInt(getSelectedIndexValue(videoFpsSelect));
screenMaxFrameRate = parseInt(getSelectedIndexValue(screenFpsSelect), 10);
videoMaxFrameRate = parseInt(getSelectedIndexValue(videoFpsSelect), 10);
notifyBySound = lsSettings.sounds;
isAudioPitchBar = lsSettings.pitch_bar;
switchSounds.checked = notifyBySound;
@@ -4538,18 +4604,15 @@ function setupVideoUrlPlayer() {
}
/**
* Refresh Local media audio video in - out
* Camera mirror
*/
async function refreshLocalMedia() {
console.log('Refresh local media', {
audioStatus: myAudioStatus,
videoStatus: myVideoStatus,
});
// some devices can't swap the video track, if already in execution.
await stopLocalVideoTrack();
await stopLocalAudioTrack();
const audioVideoConstraints = await getAudioVideoConstraints();
navigator.mediaDevices.getUserMedia(audioVideoConstraints).then(gotStream).then(gotDevices).catch(handleError);
async function handleLocalCameraMirror() {
await setMyVideoStatusTrue();
// This fix IPadPro - Tablet mirror of the back camera
if ((isMobileDevice || isIPadDevice || isTabletDevice) && !isCamMirrored) {
myVideo.classList.toggle('mirror');
isCamMirrored = true;
}
}
/**
@@ -4582,7 +4645,7 @@ async function getAudioVideoConstraints() {
* @returns {object} video constraints
*/
async function getVideoConstraints(videoQuality) {
const frameRate = { max: videoMaxFrameRate };
const frameRate = videoMaxFrameRate;
switch (videoQuality) {
case 'default':
@@ -4657,10 +4720,10 @@ async function getAudioConstraints() {
* @returns void
*/
async function refreshConstraints(stream, maxFrameRate) {
if (!useVideo || stream.getVideoTracks().length == 0) return;
if (!useVideo || !hasVideoTrack(stream)) return;
stream
.getVideoTracks()[0]
.applyConstraints({ frameRate: { max: maxFrameRate } })
.applyConstraints({ frameRate: maxFrameRate })
.then(() => {
logStreamSettingsInfo('refreshConstraints', stream);
})
@@ -4675,10 +4738,10 @@ async function refreshConstraints(stream, maxFrameRate) {
* @param {string} maxFrameRate desired max frame rate
*/
function setLocalMaxFps(maxFrameRate) {
if (!useVideo) return;
if (!useVideo || !localVideoMediaStream) return;
localVideoMediaStream
.getVideoTracks()[0]
.applyConstraints({ frameRate: { max: maxFrameRate } })
.applyConstraints({ frameRate: maxFrameRate })
.then(() => {
logStreamSettingsInfo('setLocalMaxFps', localVideoMediaStream);
})
@@ -4692,7 +4755,7 @@ function setLocalMaxFps(maxFrameRate) {
* Set local video quality: https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamTrack/applyConstraints
*/
async function setLocalVideoQuality() {
if (!useVideo) return;
if (!useVideo || !localVideoMediaStream) return;
let videoConstraints = await getVideoConstraints(videoQualitySelect.value ? videoQualitySelect.value : 'default');
localVideoMediaStream
.getVideoTracks()[0]
@@ -4741,95 +4804,6 @@ function attachSinkId(element, sinkId) {
}
}
/**
* Got Stream and append to local media
* @param {object} stream media stream audio - video
* @returns {object} media Devices Info
*/
async function gotStream(stream) {
const videoFPS = isScreenStreaming ? screenMaxFrameRate : videoMaxFrameRate;
await refreshConstraints(stream, videoFPS);
await refreshMyLocalStream(stream, true);
await refreshMyStreamToPeers(stream, true);
if (myVideoChange) {
setMyVideoStatusTrue();
// This fix IPadPro - Tablet mirror of the back camera
if ((isMobileDevice || isIPadDevice || isTabletDevice) && !isCamMirrored) {
myVideo.classList.toggle('mirror');
isCamMirrored = true;
}
}
// Refresh button list in case labels have become available
return navigator.mediaDevices.enumerateDevices();
}
/**
* Get audio-video Devices and show it to select box
* https://webrtc.github.io/samples/src/content/devices/input-output/
* https://github.com/webrtc/samples/tree/gh-pages/src/content/devices/input-output
* @param {object} deviceInfos device infos
*/
function gotDevices(deviceInfos) {
// Handles being called several times to update labels. Preserve values.
const values = selectors.map((select) => select.value);
selectors.forEach((select) => {
while (select.firstChild) {
select.removeChild(select.firstChild);
}
});
// check devices
for (let i = 0; i !== deviceInfos.length; ++i) {
const deviceInfo = deviceInfos[i];
// console.log("device-info ------> ", deviceInfo);
const option = document.createElement('option');
option.value = deviceInfo.deviceId;
switch (deviceInfo.kind) {
case 'videoinput':
option.text = `📹 ` + deviceInfo.label || `📹 camera ${videoSelect.length + 1}`;
videoSelect.appendChild(option);
break;
case 'audioinput':
option.text = `🎤 ` + deviceInfo.label || `🎤 microphone ${audioInputSelect.length + 1}`;
audioInputSelect.appendChild(option);
break;
case 'audiooutput':
option.text = `🔈 ` + deviceInfo.label || `🔈 speaker ${audioOutputSelect.length + 1}`;
audioOutputSelect.appendChild(option);
break;
default:
console.log('Some other kind of source/device: ', deviceInfo);
}
} // end for devices
selectors.forEach((select, selectorIndex) => {
if (Array.prototype.slice.call(select.childNodes).some((n) => n.value === values[selectorIndex])) {
select.value = values[selectorIndex];
}
});
}
/**
* Handle getUserMedia error: https://blog.addpipe.com/common-getusermedia-errors/
* @param {object} err user media error
*/
function handleError(err) {
console.error('navigator.MediaDevices.getUserMedia error: ', err);
switch (err.name) {
case 'OverconstrainedError':
userLog(
'error',
"GetUserMedia: Your device doesn't support the selected video quality or fps, please select the another one.",
);
break;
default:
userLog('error', 'GetUserMedia error ' + err);
}
}
/**
* AttachMediaStream stream to element
* @param {object} element element to attach the stream
@@ -5018,6 +4992,8 @@ function handleAudio(e, init, force = null) {
const audioStatus = force !== null ? force : !localAudioMediaStream.getAudioTracks()[0].enabled;
const audioClassName = audioStatus ? className.audioOn : className.audioOff;
myAudioStatus = audioStatus;
localAudioMediaStream.getAudioTracks()[0].enabled = audioStatus;
force != null ? (e.className = audioClassName) : (e.target.className = audioClassName);
@@ -5032,23 +5008,35 @@ function handleAudio(e, init, force = null) {
lS.setInitConfig(lS.MEDIA_TYPE.audio, audioStatus);
}
myAudioStatus = audioStatus;
setMyAudioStatus(myAudioStatus);
}
/**
* Stop audio track from MediaStream
* @param {MediaStream} stream
*/
async function stopAudioTracks(stream) {
if (!stream) return;
stream.getTracks().forEach((track) => {
if (track.kind === 'audio') track.stop();
});
}
/**
* Handle Video ON - OFF
* @param {object} e event
* @param {boolean} init on join room
* @param {null|boolean} force video off (default null can be true/false)
*/
function handleVideo(e, init, force = null) {
async function handleVideo(e, init, force = null) {
if (!useVideo) return;
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream/getVideoTracks
const videoStatus = force !== null ? force : !localVideoMediaStream.getVideoTracks()[0].enabled;
const videoClassName = videoStatus ? className.videoOn : className.videoOff;
myVideoStatus = videoStatus;
localVideoMediaStream.getVideoTracks()[0].enabled = videoStatus;
force != null ? (e.className = videoClassName) : (e.target.className = videoClassName);
@@ -5063,8 +5051,37 @@ function handleVideo(e, init, force = null) {
lS.setInitConfig(lS.MEDIA_TYPE.video, videoStatus);
}
myVideoStatus = videoStatus;
setMyVideoStatus(myVideoStatus);
if (!videoStatus) {
if (!isScreenStreaming) {
// Stop the video track based on the condition
if (init) {
await stopVideoTracks(initStream); // Stop init video track (camera LED off)
} else {
await stopVideoTracks(localVideoMediaStream); // Stop local video track (camera LED off)
}
}
} else {
if (init) {
// Resume the video track for the init camera (camera LED on)
await changeInitCamera(initVideoSelect.value);
} else if (!isScreenStreaming) {
// Resume the video track for the local camera (camera LED on)
await changeLocalCamera(videoSelect.value);
}
}
setMyVideoStatus(videoStatus);
}
/**
* Stop video track from MediaStream
* @param {MediaStream} stream
*/
async function stopVideoTracks(stream) {
if (!stream) return;
stream.getTracks().forEach((track) => {
if (track.kind === 'video') track.stop();
});
}
/**
@@ -5132,10 +5149,10 @@ async function stopLocalAudioTrack() {
async function toggleScreenSharing(init = false) {
try {
// Set screen frame rate
screenMaxFrameRate = parseInt(screenFpsSelect.value);
screenMaxFrameRate = parseInt(screenFpsSelect.value, 10);
const constraints = {
audio: false,
video: { frameRate: { max: screenMaxFrameRate } },
video: { frameRate: screenMaxFrameRate },
};
// Store webcam video status before screen sharing
@@ -5166,10 +5183,11 @@ async function toggleScreenSharing(init = false) {
} else {
emitPeersAction('screenStop');
adaptAspectRatio();
await refreshConstraints(screenMediaPromise, videoMaxFrameRate);
//await refreshConstraints(screenMediaPromise, videoMaxFrameRate);
}
await emitPeerStatus('screen', myScreenStatus);
await stopLocalVideoTrack();
await refreshMyLocalStream(screenMediaPromise);
await refreshMyStreamToPeers(screenMediaPromise);
@@ -5398,21 +5416,23 @@ async function refreshMyLocalStream(stream, localAudioTrackChange = false) {
// enable video
if (useVideo || isScreenStreaming) stream.getVideoTracks()[0].enabled = true;
// enable audio
if (localAudioTrackChange && myAudioStatus === false) {
// enable audio if changed and disabled
if (localAudioTrackChange && !myAudioStatus) {
audioBtn.className = className.audioOn;
setMyAudioStatus(true);
myAudioStatus = true;
}
const tracksToInclude = [];
const videoTrack = hasVideoTrack(stream)
? stream.getVideoTracks()[0]
: localVideoMediaStream && localVideoMediaStream.getVideoTracks()[0];
: hasVideoTrack(localVideoMediaStream) && localVideoMediaStream.getVideoTracks()[0];
const audioTrack =
hasAudioTrack(stream) && localAudioTrackChange
? stream.getAudioTracks()[0]
: localAudioMediaStream && localAudioMediaStream.getAudioTracks()[0];
: hasAudioTrack(localAudioMediaStream) && localAudioMediaStream.getAudioTracks()[0];
// https://developer.mozilla.org/en-US/docs/Web/API/MediaStream
if (useVideo || isScreenStreaming) {
@@ -5440,21 +5460,19 @@ async function refreshMyLocalStream(stream, localAudioTrackChange = false) {
}
}
// refresh video privacy mode on screen sharing
if (isScreenStreaming) {
// refresh video privacy mode on screen sharing
isVideoPrivacyActive = false;
setVideoPrivacyStatus('myVideo', isVideoPrivacyActive);
}
// adapt video object fit on screen streaming
getId('myVideo').style.objectFit = isScreenStreaming ? 'contain' : 'var(--video-object-fit)';
// on toggleScreenSharing video stop from popup bar
if (useVideo || isScreenStreaming) {
// on toggleScreenSharing video stop from popup bar
stream.getVideoTracks()[0].onended = () => {
toggleScreenSharing();
};
}
// adapt video object fit on screen streaming
getId('myVideo').style.objectFit = isScreenStreaming ? 'contain' : 'var(--video-object-fit)';
}
/**
@@ -5612,11 +5630,11 @@ function startMobileRecording(options, audioMixerTracks) {
*/
function startDesktopRecording(options, audioMixerTracks) {
// Get the desired frame rate for screen recording
screenMaxFrameRate = parseInt(screenFpsSelect.value);
screenMaxFrameRate = parseInt(screenFpsSelect.value, 10);
// Define constraints for capturing the screen
const constraints = {
video: { frameRate: { max: screenMaxFrameRate } },
video: { frameRate: screenMaxFrameRate },
};
// Request access to screen capture using the specified constraints
@@ -6915,6 +6933,7 @@ function setMyHandStatus() {
* @param {boolean} status of my audio
*/
function setMyAudioStatus(status) {
console.log('My audio status', status);
const audioClassName = status ? className.audioOn : className.audioOff;
audioBtn.className = audioClassName;
myAudioStatusIcon.className = audioClassName;
@@ -6923,7 +6942,6 @@ function setMyAudioStatus(status) {
setTippy(myAudioStatusIcon, status ? 'My audio is on' : 'My audio is off', 'bottom');
setTippy(audioBtn, status ? 'Stop the audio' : 'Start the audio', placement);
status ? playSound('on') : playSound('off');
console.log('My audio status', status);
}
/**
@@ -6935,8 +6953,10 @@ function setMyVideoStatus(status) {
// on vdeo OFF display my video avatar name
if (myVideoAvatarImage) myVideoAvatarImage.style.display = status ? 'none' : 'block';
if (myVideoStatusIcon) myVideoStatusIcon.className = status ? className.videoOn : className.videoOff;
// send my video status to all peers in the room
emitPeerStatus('video', status);
if (!isMobileDevice) {
if (myVideoStatusIcon) setTippy(myVideoStatusIcon, status ? 'My video is on' : 'My video is off', 'bottom');
setTippy(videoBtn, status ? 'Stop the video' : 'Start the video', placement);