Youtube API event distinguish pause from seek/buffer - javascript

We've set up event tracking for videos start/play/pause/complete events.
We have a unified way of reporting the events for HTML5, Youtube and Youku videos. For HTML5 and mobile Youku videos there's no issue. For desktop Youku (flash) we've had to set up a setInterval poll to check what state the video is in, it's not pretty but it works well.
The issue is with the Youtube embedded video. We listen to the onStateChange event:
player.addEventListener('onStateChange', function(e) {
if(e.data === 0) {
// Complete
} else if(e.data === 1) {
// Play
} else if(e.data === 2) {
// Pause
}
}
But when the user seeks in the video while the video is playing, interacts with the timeline bar, the player will trigger a pause a play and a buffer (e.data === 3) event. We do not want to track the pause and play caused by seeking.
In chrome we can distinguish the seek action since the buffer event will always trigger first. Like 3, 2 and when the player is done buffering 1. So we ignore any pause event that closely follows a buffer event, and any play event following a buffer event regardless of time passed. This works well.
In firefox however the sequence of events is very different. In firefox the buffer event is trailing. So we get the order 2, 1, 3. If the video is already buffering we get 2, 3, 1.
Is there another way of detecting seek events for youtube videos? Or a way to capture sequences?

This is how I ended up solving this issue. It will track play and pause events but not track anything when the user seeks. As far as I can tell it works as expected in the browsers I've tested.
var youtubeTrackingGate = youtubeTrackingGateFactory();
youtubePlayer.addEventListener('onStateChange', function(e) {
if(e.data === -1 || e.data === 3) {
youTubeTrackingGate({type: e.data});
} else if(e.data === 1) {
youTubeTrackingGate({type: e.data, event: 'PLAY'});
} else if(e.data === 2) {
youTubeTrackingGate({type: e.data, event: 'PAUSE'});
}
});
function youtubeTrackingGateFactory () {
var
sequence = [],
preventNextTracking = false,
data,
timeout;
return function(obj) {
// Chrome seek event
if(util.compareArrays(sequence, [3]) && obj.type === 2) {
preventNextTracking = true;
// Prevent next play
} else if(preventNextTracking && obj.type === 1) {
preventNextTracking = false;
} else {
clearTimeout(timeout);
// Save event
sequence.push(obj.type);
data = obj.event;
timeout = setTimeout(function() {
// Single event, let it pass if it's (play/pause)
if(sequence.length === 1 && [1, 2].indexOf(sequence[0]) > -1) {
sendTracking(data);
}
sequence = [];
}, 500);
}
// Suppress any (play/pause) after buffer event
if(obj.type === 3) {
// If not inital play
if(!util.compareArrays(sequence, [-1, 3])) {
preventNextTracking = true;
// If is initial play
} else {
sequence = [];
}
}
};
}
sendTracking(event) {
// code
}

Related

How to play sound in javascript?

I create websocket server in python to handle notification event. Now, i can receive notification, the problem is i can't play sound because new autoplay policy changed, if i play sound using javascript it give me domexception. Any suggestion please ?
As i know, playing sound is simple in html-javascript. like this example: https://stackoverflow.com/a/18628124/7514010
but it depend to your browsers and how you load and play, so issues is:
Some of browsers wait till user click something, then let you play it (Find a way for it)
In some case browsers never let you play till the address use SSL (means the HTTPS behind your url)
The loading be late so the playing be late / or even not start.
So i usually do this:
HTML
<audio id="notifysound" src="notify.mp3" autobuffer preload="auto" style="visibility:hidden;width:0px;height:0px;z-index:-1;"></audio>
JAVASCRIPT (Generally)
var theSound = document.getElementById("notifysound");
theSound.play();
And the most safe if i want sure it be played when i notify is :
JAVASCRIPT (In your case)
function notifyme(theTitle,theBody) {
theTitle=theTitle || 'Title';
theBody=theBody || "Hi. \nIt is notification!";
var theSound = document.getElementById("notifysound");
if ("Notification" in window && Notification) {
if (window.Notification.permission !== "granted") {
window.Notification.requestPermission().then((result) => {
if (result != 'denied') {
return notifyme(theTitle,theBody);
} else {
theSound.play();
}
});
} else {
theSound.play();
try {
var notification = new Notification(theTitle, {
icon: 'icon.png',
body: theBody
});
notification.onclick = function () {
window.focus();
};
}
catch(err) {
return;
}
}
} else {
theSound.play();
}
}
(and just hope it be played. because even possible to volume or some customization make it failed.)
to bypass new autoplay policy :
create a button that can play the sound, hide it and trigger the sound with :
var event = new Event('click');
playBtn.dispatchEvent(event);
EDIT
assuming you have :
let audioData = 'data:audio/wav;base64,..ᴅᴀᴛᴀ...'; // or the src path
you can use this function to trigger whenever you want without appending or create element to the DOM:
function playSound() {
let audioEl = document.createElement('audio');
audioEl.src = audioData;
let audioBtn = document.createElement('button');
audioBtn.addEventListener('click', () => audioEl.play(), false);
let event = new Event('click');
audioBtn.dispatchEvent(event);
}
usage :
just playSound()
EDIT 2
I re test my code and it does'nt work hum ... weird

AudioContext does not have a pause property?

I have used javascript Audio() before, but now I need to add some reverb effect in the audio and I am using reverb.js which uses the AudioContext api. I have the start property available, but no pause property? How do I pause or stop the audio??
Here is my code:
<script src="http://reverbjs.org/reverb.js"></script>
<script>
// 1) Setup your audio context (once) and extend with Reverb.js.
var audioContext = new (window.AudioContext || window.webkitAudioContext)();
reverbjs.extend(audioContext);
// 2) Load the impulse response; upon load, connect it to the audio output.
var reverbUrl = "http://reverbjs.org/Library/SaintLawrenceChurchMolenbeekWersbeekBelgium.m4a";
var reverbNode = audioContext.createReverbFromUrl(reverbUrl, function() {
reverbNode.connect(audioContext.destination);
});
// 3) Load a test sound; upon load, connect it to the reverb node.
var sourceUrl = "./sample.mp3";
var sourceNode = audioContext.createSourceFromUrl(sourceUrl, function() {
sourceNode.connect(reverbNode);
});
</script>
Play
Stop
Also, I tried using stop(), and it works, but when I fire start() after clicking on stop, the start() doesn't work. Can you you help me out with a solution??
You can use the suspend() and resume() methods of AudioContext to pause and resume your audio: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/suspend
One way to implement this with a single button for play/pause/resume, would be to add a function that controls the player state. For example:
let started = false;
function pauseOrResume() {
if (!started) {
sourceNode.start();
started = true;
document.getElementById("pauseButton").innerHTML = 'Pause';
} else if (audioContext.state === 'running') {
audioContext.suspend().then(function () {
document.getElementById("pauseButton").innerHTML = 'Resume';
});
} else if (audioContext.state === 'suspended') {
audioContext.resume().then(function () {
document.getElementById("pauseButton").innerHTML = 'Pause';
});
}
}
And replace your existing "Play" button with:
<a id="pauseButton" href="javascript:pauseOrResume()">Play</a>
This does the following:
If the audio hasn't yet been started, the link will say "Play".
If the user clicks "Play", the audio will start playing and the text of the link will change to "Pause".
If the user clicks "Pause" while the audio is playing, it will be paused, and the text of the link will change to "Resume".

How can I check whether user is currently using the audio seekbar?

I have an HTML audio player like this:
<audio id="audioPlayer" controls>
<source src="test.mp3">
</audio>
I want to display some images in sync the audio file, including when the user is moving the seekbar.
However, I can't find a way to check whether user is currently using the audio seekbar.
I have tried to use the timeupdateevent with no success: the code below works only when user seeks back in time.
var audioPlayer = document.getElementById('audioPlayer');
var lastUpdateTime;
audioPlayer.addEventListener('timeupdate', function() {update();});
function update() {
if ( audioPlayer.currentTime - lastUpdateTime < 0 )
console.log("seeking");
lastUpdateTime = audioPlayer.currentTime;
}
I am looking for something working on "recent" browsers (e.g. IE10+).
It's a bit hacky but works:
Version with jQuery:
var $audio = $( '#myAudio' );
var onPause = false;
// Pause event helps us to know is player playing or not
$audio.on( 'pause', function() {
onPause = true;
setTimeout(function() {
onPause = false;
});
});
$audio[0].on( 'timeupdate', function(e) {
// trick to get current pause state
setTimeout(function(){
// checks if player paused and not last timeupdate event call
if ( $audio[0].paused && !onPause ) {
// Fire event then user is changing seek bar
$audio.trigger( 'userSeeking' );
}
});
});
$audio.on( 'userSeeking', function(){
// do some magic
});
Version with pure javascript:
var audio = document.getElementById( 'myAudio' );
var onPause = false;
var seek = false;
// Pause event helps us to know is player playing or not
audio.addEventListener( 'pause', function(e) {
onPause = true;
setTimeout(function() {
onPause = false;
});
});
audio.addEventListener( 'timeupdate', function(e) {
// trick to get current pause state
setTimeout(function(){
seek = false;
// checks if player paused and not last timeupdate event call
if ( $audio[0].paused && !onPause ) {
seek = true;
// Fire event then user is changing seek bar
}
// or you can return current state of seeking here
});
});
And here is working example ( codepen using jQuery version ):
http://codepen.io/GomatoX/pen/ZYpWbN

HTML5 video, how to detect when there is no audio track?

I'm making a chrome app and I'd like to have custom controls for the video playback but I'm having some difficulties with the mute button. Most of the videos that will be played in the app are silent so I'd like to be able to disable the button when there is no audio track just like chrome does with the default controls.
Tried using the volume value but it returns "1" even though there's no audio track. Checking if the video is muted didn't work either.
Here's a snippet.
Any suggestions?
Shorter function based on upuoth's answer and extended to support IE10+
function hasAudio (video) {
return video.mozHasAudio ||
Boolean(video.webkitAudioDecodedByteCount) ||
Boolean(video.audioTracks && video.audioTracks.length);
}
Usage:
var video = document.querySelector('video');
if(hasAudio(video)) {
console.log("video has audio");
} else{
console.log("video doesn't have audio");
}
At some point, browsers might start implementing the audioTracks property. For now, you can use webkitAudioDecodedByteCount for webkit, and mozHasAudio for firefox.
document.getElementById("video").addEventListener("loadeddata", function() {
if (typeof this.webkitAudioDecodedByteCount !== "undefined") {
// non-zero if video has audio track
if (this.webkitAudioDecodedByteCount > 0)
console.log("video has audio");
else
console.log("video doesn't have audio");
}
else if (typeof this.mozHasAudio !== "undefined") {
// true if video has audio track
if (this.mozHasAudio)
console.log("video has audio");
else
console.log("video doesn't have audio");
}
else
console.log("can't tell if video has audio");
});
For some reason #fregante's hasAudio function stopped working in Chrome at some point - even after waiting for the "loadeddata" and "loadedmetadata" events, and even the "canplaythrough" event. It may have something to do with the video format I'm using (webm). In any case, I solved it by playing the video for a short amount of time:
// after waiting for the "canplaythrough" event:
hasAudio(video); // false
video.play();
await new Promise(r => setTimeout(r, 1000));
video.pause();
hasAudio(video); // true
There are different ways to check whether a video file has audio or not, one of which is to use mozHasAudio, video.webkitAudioDecodedByteCount and video.audioTracks?.length properties of video, clean and simple...
const video = component.node.querySelector("video");
video.onloadeddata = function() {
if ((typeof video.mozHasAudio !== "undefined" && video.mozHasAudio) ||
(typeof video.webkitAudioDecodedByteCount !== "undefined" && video.webkitAudioDecodedByteCount > 0) ||
Boolean(video.audioTracks?.length)) {
console.log("This video has audio tracks.");
} else {
console.log("This video has no audio tracks.");
}
};

Keeping two YouTube videos in sync with each other

I've got two identical YouTube videos embedded on the same page. I'd like them to both be in sync, here are my requirements / notes:
Both videos must start at the same time
When a video is played / paused by the user the other video does the same
This is quite easy via the API
When one video buffers the other stops to wait, and starts when they are both ready
I only need audio from one video
Sync accuracy doesn't have to be millisecond perfect, just reliable
One video will be used as a background video
This video will be slightly blurred (using CSS3 blur), so quality not super essential
I've tried using the YouTube JS API to listen for player state changes and attempt to keep both videos in sync, however it wasn't as reliable as I'd hoped for. I'll post part of the code for this below.
One caveat is that one video will appear larger than the other, so the YouTube might provide a higher quality video for that.
Because I'm using CSS3 blur I can only use recent Webkit browsers, so a solution that works on these alone (and not FF/IE) is not a problem.
My question is this, for the requirements above, is there any way to keep these two videos in sync? I did consider if it was possible to use the canvas API to "redraw" the video, but after researching figured this wasn't going to be possible.
buffering = false;
var buffer_control = function(buffering_video, sibling_video, state) {
switch ( state ) {
case 1: // play
if ( buffering === true ) {
console.error('restarting after buffer');
// pause both videos, we want to make sure they are both at the same time
buffering_video.pauseVideo();
sibling_video.pauseVideo();
// get the current time of the video being buffered...
var current_time = buffering_video.getCurrentTime();
// ... and pull the sibling video back to that time
sibling_video.seekTo(current_time, true);
// finally, play both videos
buffering_video.playVideo();
sibling_video.playVideo();
// unset the buffering flag
buffering = false;
}
break;
case 3: // buffering
console.error('buffering');
// set the buffering flag
buffering = true;
// pause the sibling video
sibling_video.pauseVideo();
break;
}
}
Your project is kinda interesting, that's why I decided to try to help you. I have never used the youtube API but I have tried some coding and it might be a start for you.
So here is the code I have tried and it seems to work quite well , it certainly needs some improvements ( I haven't tried to calculate the offset between the two played videos but letting them unmute shows the mismatch and it sounds legit)
Here we go :
Let's start with some html basics
<!DOCTYPE html>
<html>
<head>
We add an absolute positionning for the foreground player so it overlays the one playing the background video (for testing)
<style>
#player2{position:absolute;left:195px;top:100px;}
</style>
</head>
<body>
jQuery is used here to fade in/out the players (you'll see why below) but you can use basic JS
<script src="jquery-1.10.2.min.js"></script>
The < iframes> (and video players) will replace these < div> tags.
<div id="player1"></div> <!-- Background video player -->
<div id="player2"></div> <!-- Foreground video player -->
<script>
This code loads the IFrame Player API code asynchronously.
var tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
var firstScriptTag = document.getElementsByTagName('script')[0];
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
This function creates the < iframes> (and YouTube players) after the API code downloads. Note the callback functions : onPlayer1Ready and onPlayer1StateChange
var player1;
var player2;
function onYouTubeIframeAPIReady() {
player1 = new YT.Player('player1', {
height: '780',
width: '1280',
videoId: 'M7lc1UVf-VE',
playerVars: { 'autoplay': 0, 'controls': 0 },
events: {
'onReady': onPlayer1Ready,
'onStateChange': onPlayer1StateChange
}
});
player2 = new YT.Player('player2', {
height: '390',
width: '640',
videoId: 'M7lc1UVf-VE',
events: {
'onReady': onPlayer2Ready,
'onStateChange': onPlayer2StateChange
}
});
}
var player1Ready = false;
var player2Ready = false;
So now is the interesting part of the code. The main issue in your project of sync is linked to the fact that videos need to be buffered before launching them. Actually the API doesn't provide any kind of intuitive function to preload the videos (due to bandwidth issues (client and server side). So we have to get a bit tricky.
The steps to preload a video are the following:
Hide the player so the next steps aren't visible for the user);
Mute the player ( player.mute():Void );
Emulate a jump in timeline to start the buffering ( player.seekTo(seconds:Number, allowSeekAhead:Boolean):Void );
Wait for a state change event equal to YT.PlayerState.PLAYING;
Pause the video ( player.pauseVideo():Void );
Rewind the video with player.seekTo(seconds:Number, allowSeekAhead:Boolean):Void ;
Unmute the player ( player.unMute():Void );
Show the player.
You have to preload your two videos.
var preloading1 = false;
var preloading2 = false;
The API will call these functions when the video players are ready.
function onPlayer1Ready(event)
{
player1Ready = true;
preloading1 = true; // Flag the player 1 preloading
player1.mute(); // Mute the player 1
$( "#player1" ).hide(); // Hide it
player1.seekTo(1); // Start the preloading and wait a state change event
}
function onPlayer2Ready(event) {
player2Ready = true; // The foreground video player is not preloaded here
}
The API calls this function when the background video player's state changes.
function onPlayer1StateChange(event)
{
if (event.data == YT.PlayerState.PLAYING ) {
if(preloading1)
{
prompt("Background ready"); // For testing
player1.pauseVideo(); // Pause the video
player1.seekTo(0); // Rewind
player1.unMute(); // Comment this after test
$( "#player1" ).show(); // Show the player
preloading1 = false;
player2Ready = true;
preloading2 = true; // Flag for foreground video preloading
player2.mute();
//$( "#player2" ).hide();
player2.seekTo(1); // Start buffering and wait the event
}
else
player2.playVideo(); // If not preloading link the 2 players PLAY events
}
else if (event.data == YT.PlayerState.PAUSED ) {
if(!preloading1)
player2.pauseVideo(); // If not preloading link the 2 players PAUSE events
}
else if (event.data == YT.PlayerState.BUFFERING ) {
if(!preloading1)
{
player2.pauseVideo(); // If not preloading link the 2 players BUFFERING events
}
}
else if (event.data == YT.PlayerState.CUED ) {
if(!preloading1)
player2.pauseVideo(); // If not preloading link the 2 players CUEING events
}
else if (event.data == YT.PlayerState.ENDED ) {
player2.stopVideo(); // If not preloading link the 2 players ENDING events
}
}
The API calls this function when the foreground video player's state changes.
function onPlayer2StateChange(event) {
if (event.data == YT.PlayerState.PLAYING ) {
if(preloading2)
{
//prompt("Foreground ready");
player2.pauseVideo(); // Pause the video
player2.seekTo(0); // Rewind
player2.unMute(); // Unmute
preloading2 = false;
$( "#player2" ).show(50, function() {
Here is a part of the code that acts strangely. Uncommenting the line below will make the sync quite bad,but if you comment it, you will have to click twice on the PLAY button BUT the sync will look way better.
//player2.playVideo();
});
}
else
player1.playVideo();
}
else if (event.data == YT.PlayerState.PAUSED ) {
if(/*!preloading1 &&*/ !preloading2)
player1.pauseVideo();
}
else if (event.data == YT.PlayerState.BUFFERING ) {
if(!preloading2)
{
player1.pauseVideo();
//player1.seekTo(... // Correct the offset here
}
}
else if (event.data == YT.PlayerState.CUED ) {
if(!preloading2)
player1.pauseVideo();
}
else if (event.data == YT.PlayerState.ENDED ) {
player1.stopVideo();
}
}
</script>
</body>
</html>
Note that the views might not be counted with this code.
If you want the code without the explanations you can go here : http://jsfiddle.net/QtBlueWaffle/r8gvX/1/
2016 Update
Live Preview
Hope this helps.

Categories