I'm creating video objects via jquery from an array of urls that point to mp4 files.
I want to have the first video autoplay then once its finished pick up on the "ended" function and play the next video in the sequence.
$(document).ready(function () {
var urls = ["https://s3-us-west-1.amazonaws.com/linkto.mp4", "https://s3-us-west-1.amazonaws.com/linktoother.mp4"]
var videos = [];
for (var i = 0; i < urls.length; i++) {
var path = urls[i];
var video = $('<video></video>');
var source = $('<source></source>');
source.attr('src', path);
source.appendTo(video);
if (i === 0) {
video.attr('autoplay', 'true');
}
video.attr('id', 'video_' + i);
video.attr('preload', 'true');
video.attr('width', '100%');
videos.push(video);
video.on('ended', function () {
console.log('Video has ended!' + i);
// playNext(video);
});
$('#movie').append(video);
}
I can see the movies generated and the first one plays fine. The problem is when 'ended' function the variable i == 2 because thats how many videos are in the array and the for loop increased i. I've been away from javascript for a while. I can think of workarounds like adding an index attribute to the video element or something but basically how do I get the correct index in the "ended" function.
The problem here is the wrong use of a closure variable in a loop.
In your case you can create a local closure by using $.each() to iterate over the array
$(document).ready(function () {
var urls = ["https://s3-us-west-1.amazonaws.com/linkto.mp4", "https://s3-us-west-1.amazonaws.com/linktoother.mp4"]
var videos = [];
$.each(urls, function (i, path) {
var video = $('<video></video>');
var source = $('<source></source>');
source.attr('src', path);
source.appendTo(video);
if (i === 0) {
video.attr('autoplay', 'true');
}
video.attr('id', 'video_' + i);
video.attr('preload', 'true');
video.attr('width', '100%');
videos.push(video);
video.on('ended', function () {
console.log(i)
});
$('#movie').append(video);
})
})
You really don't want to have the index, since all the video elements are siblings you can just find the next sibling of the currently ended video element
video.on('ended', function () {
console.log('Video has ended!' + $(this).index());
var $current = $(this),
$next = $this.next();
console.log($next.find('source').attr('src'))
//playNext($(this));
});
Creating closures in loops: A common mistake
JavaScript closure inside loops – simple practical example
Related
I want to be able to change the value of a global variable when it is being used by a function as a parameter.
My javascript:
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
playAudio(audioFilePitch01, canPlayPitch01);
});
My HTML:
<body>
<button id="btnPitch01">Play Pitch01</button>
<button id="btnPitch02">Play Pitch02</button>
<script src="js/js-master.js"></script>
</body>
My scenario:
I'm building a Musical Aptitude Test for personal use that won't be hosted online. There are going to be hundreds of buttons each corresponding to their own audio files. Each audio file may only be played twice and no more than that. Buttons may not be pressed while their corresponding audio files are already playing.
All of that was working completely fine, until I optimised the function to use parameters. I know this would be good to avoid copy-pasting the same function hundreds of times, but it has broken the solution I used to prevent the audio from being played more than once. The "canPlayPitch01" variable, when it is being used as a parameter, no longer gets incremented, and therefore makes the [if (canPlay < 2)] useless.
How would I go about solving this? Even if it is bad coding practise, I would prefer to keep using the method I'm currently using, because I think it is a very logical one.
I'm a beginner and know very little, so please forgive any mistakes or poor coding practises. I welcome corrections and tips.
Thank you very much!
It's not possible, since variables are passed by value, not by reference. You should return the new value, and the caller should assign it to the variable.
function playAudio(audioFile, canPlay) {
if (canPlay < 2 && audioFile.paused) {
canPlay = canPlay + 1;
audioFile.play();
} else {
if (canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
return canPlay;
};
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
btnPitch01.addEventListener("click", function() {
canPlayPitch01 = playAudio(audioFilePitch01, canPlayPitch01);
});
A little improvement of the data will fix the stated problem and probably have quite a few side benefits elsewhere in the code.
Your data looks like this:
const btnPitch01 = document.getElementById("btnPitch01");
const audioFilePitch01 = new Audio("../aud/Pitch01.wav");
var canPlayPitch01 = 0;
// and, judging by the naming used, there's probably more like this:
const btnPitch02 = document.getElementById("btnPitch02");
const audioFilePitch02 = new Audio("../aud/Pitch02.wav");
var canPlayPitch02 = 0;
// and so on
Now consider that global data looking like this:
const model = {
btnPitch01: {
canPlay: 0,
el: document.getElementById("btnPitch01"),
audioFile: new Audio("../aud/Pitch01.wav")
},
btnPitch02: { /* and so on */ }
}
Your event listener(s) can say:
btnPitch01.addEventListener("click", function(event) {
// notice how (if this is all that's done here) we can shrink this even further later
playAudio(event);
});
And your playAudio function can have a side-effect on the data:
function playAudio(event) {
// here's how we get from the button to the model item
const item = model[event.target.id];
if (item.canPlay < 2 && item.audioFile.paused) {
item.canPlay++;
item.audioFile.play();
} else {
if (item.canPlay >= 2) {
alert("This audio has already been played twice.");
} else {
alert("Please wait for the audio to finish playing.");
};
};
};
Side note: the model can probably be built in code...
// you can automate this even more using String padStart() on 1,2,3...
const baseIds = [ '01', '02', ... ];
const model = Object.fromEntries(
baseIds.map(baseId => {
const id = `btnPitch${baseId}`;
const value = {
canPlay: 0,
el: document.getElementById(id),
audioFile: new Audio(`../aud/Pitch${baseId}.wav`)
}
return [id, value];
})
);
// you can build the event listeners in a loop, too
// (or in the loop above)
Object.values(model).forEach(value => {
value.el.addEventListener("click", playAudio)
})
below is an example of the function.
btnPitch01.addEventListener("click", function() {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio(audioFilePitch01, canPlayPitch01);
this.dataset.numberOfPlays++;
});
you would want to select all of your buttons and assign this to them after your html is loaded.
https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByClassName
const listOfButtons = document.getElementsByClassName('pitchButton');
listOfButtons.forEach( item => {
item.addEventListener("click", () => {
if ( this.dataset.numberOfPlays >= this.dataset.allowedNumberOfPlays ) return;
playAudio("audioFilePitch" + this.id);
this.dataset.numberOfPlays++;
});
I seem to have a very strange problem. I am trying to play a video which is being streamed live using a web browser. For this, I am looking at the MediaSource object. I have got it in a way so that the video is taken from a server in chunks to be played. The problem is that the first chunk plays correctly then playback stops.
To make this even more strange, if I put the computer to sleep after starting streaming, then wake it up andthe video will play as expected.
Some Notes:
I am currently using chrome.
I have tried both of them ,with and without calling MediaSource's endOfStream.
var VF = 'video/webm; codecs="vp8,opus"';
var FC = 0;
alert(MediaSource.isTypeSupported(VF));
var url = window.URL || window.webkitURL;
var VSRC = new MediaSource();
var VURL = URL.createObjectURL(VSRC);
var bgi, idx = 1;
var str, rec, dat = [], datb, brl;
var sbx;
//connect the mediasource to the <video> elment first.
vid2.src = VURL;
VSRC.addEventListener("sourceopen", function () {
// alert(VSRC.readyState);
//Setup the source only once.
if (VSRC.sourceBuffers.length == 0) {
var sb = VSRC.addSourceBuffer(VF);
sb.mode = 'sequence';
sb.addEventListener("updateend", function () {
VSRC.endOfStream();
});
sbx = sb;
}
});
//This function will be called each time we get more chunks from the stream.
dataavailable = function (e) {
//video is appended to the sourcebuffer, but does not play in video element
//Unless the computer is put to sleep then awaken!?
sbx.appendBuffer(e.result);
FC += 1;
//These checks behave as expected.
len.innerHTML = "" + sbx.buffered.length + "|" + VSRC.duration;
CTS.innerHTML = FC;
};
You are making two big mistakes:
You can only call sbx.appendBuffer when sbx.updating property is false, otherwise appendBuffer will fail. So what you need to do in reality is have a queue of chunks, and add a chunk to the queue if sbx.updating is true:
if (sbx.updating || segmentsQueue.length > 0)
segmentsQueue.push(e.result);
else
sbx.appendBuffer(e.result);
Your code explicitly says to stop playing after the very first chunk:
sb.addEventListener("updateend", function () {
VSRC.endOfStream();
});
Here is what you really need to do:
sb.addEventListener("updateend", function () {
if (!sbx.updating && segmentsQueue.length > 0) {
sbx.appendBuffer(segmentsQueue.shift());
}
});
In the below example we read from a JSON data and store it in a variable.
Then we loop through it to print out the value of each of its iteration.
I have "cartList.innerHTML" which will list out "Edit" 4 times as that is the number of the objects in the array. Like below
Edit
Edit
Edit
Edit
Once you click on the first Edit a modal should open and display the name of the first object, on clicking the second edit the name of the second and so on.
But for some reason the value remains to be the name of the last object for each Edit. How do I get it to print the correct name for each edit.
// Fetch JSON data
function loadJSON(file, callback) {
var xobj = new XMLHttpRequest();
xobj.overrideMimeType("application/json");
xobj.open('GET', file, true); // Refers to JSON file
xobj.onreadystatechange = function() {
if (xobj.readyState == 4 && xobj.status == "200") {
callback(xobj.responseText);
}
}
xobj.send(null);
}
function load() {
loadJSON("assets/cart.json", function(response) {
var actual_JSON = JSON.parse(response);
console.log(actual_JSON);
var cartList = document.getElementById("cart-products");
var itemObj = actual_JSON.productsInCart;
var itemLength = actual_JSON.productsInCart.length;
// Loop through JSON data
for (var i = 0; i < itemLength; i++) {
(function(i) {
/* Output Link Element with Edit text & on click
display a modal containing current value of iteration */
cartList.innerHTML += '<div>'+
'Edit'+
'</div>';
var editCartModal = document.getElementById("edit-cart");
editCartModal.innerHTML = itemObj[i].p_name;// Name of the current object
})(i);
// for loop ends here
}
})
}
load(); // Loads function on page load
This happens because every function inside the for loop begins to execute after the loop ends. It's the same behavior as for callbacks. When you call each (function(i) {})(i) they all go to the end of function call stack, and only after last iteration of the loop they start executing (with the last i value).
I'm curious why do you need an internal inline function at all? Why don't just put this code directly to the loop body?
It's because though you are running the following code in an IIFE:
var editCartModal = document.getElementById("edit-cart");
editCartModal.innerHTML = itemObj[i].p_name;// Name of the current object
You're just modifying the same modal (getElementById()) four times.
A simple answer to fix this would be adding a data attribute:
for (var i = 0; i < itemLength; i++) {
(function(i) {
/* Output Link Element with Edit text & on click
display a modal containing current value of iteration */
cartList.innerHTML += '<div>'+
'<a href="javascript: void(0);"' +
'class="btn btn-outline button-edit" data-toggle="modal"' +
'data-target=".bs-example-modal-lg" data-custom-value="' + i +
'">Edit</a>'+
'</div>';
})(i);
// for loop ends here
}
/* ES5 way*/
/*
var list = document.getElementsByClassName('button-edit');
list.forEach(function(){
...
})
*/
[...document.getElementsByClassName('button-edit')].map(function(element){
element.addEventListener('click', function(e){
var chosenIdx = this.dataset.customValue;
var editCartModal = document.getElementById("edit-cart");
editCartModal.innerHTML = itemObj[chosenIdx].p_name;// Name of the current object
}, false);
});
Im trying to read a local file with videos. Here is the code. I used array to add the videos and play it in sequence but it didn't play any video.
var videos = new Array(); //("BigBuck.m4v","Video.mp4","BigBuck.m4v","Video2.mp4");
var currentVideo = 0;
function nextVideo() {
// get the element
videoElement = document.getElementById("play-video");
// remove the event listener, if there is one
videoElement.removeEventListener('ended', nextVideo, false);
// update the source with the currentVideo from the videos array
videoElement.src = videos[currentVideo];
// play the video
videoElement.play()
// increment the currentVideo, looping at the end of the array
currentVideo = (currentVideo + 1) % videos.length
// add an event listener so when the video ends it will call the nextVideo function again
videoElement.addEventListener('ended', nextVideo, false);
}
function yesnoCheck() {
document.getElementById("filepicker").style.display = 'none';
}
// add change event to pick up selected files
document.getElementById("filepicker").addEventListener("change", function (event) {
var files = event.target.files;
// loop through files
for (var i = 0; i < files.length; i++) {
// select the filename for any videos
if (files[i].type == "video/mp4") {
// add the filename to the array of videos
videos.push(files[i].name); // work from webkitRelativePath;
}
};
// once videos are loaded initialize and play the first one
nextVideo();
}, false);
Any suggestions? Thank you.
You're going to have to read the contents of the video, not just set the src to it's name. You can do so by using the FileReader.
I've adjusted your code, but I haven't tested it. Here's a working example on JsFiddle, though.
var videos = new Array(); //("BigBuck.m4v","Video.mp4","BigBuck.m4v","Video2.mp4");
var currentVideo = 0;
var reader = new FileReader();
// get th eelement
var videoElement = document.getElementById("play-video");
function nextVideo() {
// remove the event listener, if there is one
videoElement.removeEventListener('ended', nextVideo, false);
// read the file contents as a data URL
reader.readAsDataURL(videos[currentVideo]);
// increment the currentVideo, looping at the end of the array
currentVideo = (currentVideo + 1) % videos.length
// add an event listener so when the video ends it will call the nextVideo function again
videoElement.addEventListener('ended', nextVideo, false);
}
function yesnoCheck() {
document.getElementById("filepicker").style.display = 'none';
}
// add change event to pick up selected files
document.getElementById("filepicker").addEventListener("change", function (event) {
var files = event.target.files;
// loop through files
for (var i = 0; i < files.length; i++) {
// select the filename for any videos
if (files[i].type == "video/mp4") {
// add the whole file to the array
videos.push(files[i]);
}
};
// once videos are loaded initialize and play the first one
nextVideo();
}, false);
// once the file is fully read
reader.addEventListener('load', function() {
// you have the data URL in reader.result
videoElement.src = reader.result;
// play the video
videoElement.play()
});
I have the following function, that is called once in my program. When there is no user interaction it starts over again and again by recursion when the array is printed out. But when a user clicks on a link the function shall be stopped first (no more output from the array) and then be started again with new parameters.
function getText(mode){
var para = mode;
var url = "getText.php?mode=" + para;
var arr = new Array();
//get array via ajax-request
var $div = $('#text_wrapper');
$.each(arr, function(index, value){
eachID = setTimeout(function(){
$div.html(value).stop(true, true).fadeIn().delay(5000).fadeOut();
if(index == (arr.length-1)){
clearTimeout(eachID);
getText(para);
}
}, 6000 * index);
});
}
My code doesn't really stop the function but calling it once more. And that eventuates in multiple outputs and overlaying each other. How can I ensure that the function stops and runs just one at a time?
$(document).ready(function() {
$("#div1, #div2").click(function(e){
e.preventDefault();
var select = $(this).attr("id");
clearTimeout(eachID);
getText(select);
});
});
You keep overwriting eachID with the latest setTimeout, so when you clearTimeout(eachID) you are only stopping the last one.
Personally, I like to do things like this:
id = 0;
timer = setInterval(function() {
// do stuff with arr[id]
id++;
if( id >= arr.length) clearInterval(timer);
},6000);
This way, you only have one timer running, and you can clear it at any time.