When the user watches a video, I would like to make 2 AJAX calls. One when the user finished watching the video and the time played is equal or more than the duration of the video (because users can rewind as well then). timePlayed>=duration && event.type=="ended". I successfully make the call for that.
Where I struggle is that I would also like to make a call when the video is watched more than 80% and the time played of the video is more than 80% as well in order to prevent the user from just fast forwarding.
In order for that to work I have to modify my videoStartedPlaying() method and this is where I come across issues as I am trying to set an interval. Now, with setting an interval, it is like an endless loop.
var video_data = document.getElementById("video");
var timeStarted = -1;
var timePlayed = 0;
var duration = 0;
// If video metadata is loaded get duration
if(video_data.readyState > 0)
getDuration.call(video_data);
//If metadata not loaded, use event to get it
else {
video_data.addEventListener('loadedmetadata', getDuration);
}
// remember time user started the video
function videoStartedPlaying() {
timeStarted = new Date().getTime()/1000;
setInterval(function(){
playedFor = new Date().getTime()/1000 - timeStarted;
checkpoint = playedFor / duration;
percentComplete = video_data.currentTime/video_data.duration;
// here I need help of how to best accomplish this
if (percentComplete >= 0.8 && checkpoint >= 0.8) {
// AJAX call here
}
}, 2000);
}
function videoStoppedPlaying(event) {
// Start time less then zero means stop event was fired vidout start event
if(timeStarted>0) {
var playedFor = new Date().getTime()/1000 - timeStarted;
timeStarted = -1;
// add the new amount of seconds played
timePlayed+=playedFor;
}
// Count as complete only if end of video was reached
if(timePlayed>=duration && event.type=="ended") {
// AJAX call here
}
}
function getDuration() {
duration = video_data.duration;
}
video_data.addEventListener("play", videoStartedPlaying);
video_data.addEventListener("playing", videoStartedPlaying);
video_data.addEventListener("ended", videoStoppedPlaying);
video_data.addEventListener("pause", videoStoppedPlaying);
I truly would appreciate any help with this as it seems like I am at my wits end.
Thanks so much!
Edit:
Thanks to the comment I came up with this:
const video = document.getElementById("video");
const set = new Set();
const percent = .8;
let toWatch;
function mediaWatched (curr) {
alert(`${curr}% of media watched`)
}
function handleMetadata(e) {
toWatch = Math.ceil(video.duration * percent);
console.log(toWatch, video.duration);
}
function handleTimeupdate (e) {
set.add(Math.ceil(video.currentTime));
let watched = Array.from(set).pop();
if (set.has(toWatch) && watched === toWatch) {
video.removeEventListener("timeupdate", handleTimeupdate);
console.log(watched);
mediaWatched(
Math.round(watched / Math.ceil(video.duration) * 100)
);
}
}
video.addEventListener("loadedmetadata", handleMetadata);
video.addEventListener("timeupdate", handleTimeupdate);
<video width="400" height="300" controls="true" poster="" id="video">
<source type="video/mp4" src="http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4" />
</video>
Now, for example, if I fast forward to around 50% length, and let it play then, it will fire whenever 80% of the movie is reached, but it shouldn't because I fast forwarded to 50% and essentially only watched 30%.
Does that make sense? How can I achieve such behavior?
per discussion in comments here's a working sample.
It includes a couple of handlers just to make life easier in setting up the array and summing the contents so you know when you have reached the 80% mark (though you may need to change that logic if you want to force them to, say, explicit watch the first 80% not just a total of 80% throughout the video).
There are a number of console.log(...) statements in there so you can watch what it's doing in the browser console window... you'll probably want to take them out before deploying for real.
I've put the hook for where to make the ajax call in the timeupdate event, but you could always use a regular setInterval timer in the main loop as well to check for the 80% and make the call there, but this seemed cleaner
most of it should be self explanatory, but do ask in comments if there's anything that's not clear...
<video controls preload="auto" id="video" width="640" height="365" muted>
<source src="http://www.sample-videos.com/video/mp4/720/big_buck_bunny_720p_2mb.mp4" type="video/mp4">
</video>
<script>
// handler to let me resize the array once we know the length
Array.prototype.resize = function(newSize, defaultValue) {
while(newSize > this.length)
this.push(defaultValue);
this.length = newSize;
}
// function to round up a number
function roundUp(num, precision) {
return Math.ceil(num * precision) / precision
}
var vid = document.getElementById("video")
var duration = 0; // will hold length of the video in seconds
var watched = new Array(0);
var reported80percent = false;
vid.addEventListener('loadedmetadata', getDuration, false);
vid.addEventListener('timeupdate',timeupdate,false)
function timeupdate() {
currentTime = parseInt(vid.currentTime);
// set the current second to "1" to flag it as watched
watched[currentTime] = 1;
// show the array of seconds so you can track what has been watched
// you'll note that simply looping over the same few seconds never gets
// the user closer to the magic 80%...
console.log(watched);
// sum the value of the array (add up the "watched" seconds)
var sum = watched.reduce(function(acc, val) {return acc + val;}, 0);
// take your desired action on the ?80% completion
if ((sum >= (duration * .8)) && !reported80percent) {
// set reported80percent to true so that the action is triggered once and only once
// could also unregister the timeupdate event to avoid calling unneeded code at this point
// vid.removeEventListener('timeupdate',timeupdate)
reported80percent = true;
console.log("80% watched...")
// your ajax call to report progress could go here...
}
}
function getDuration() {
console.log("duration:" + vid.duration)
// get the duration in seconds, rounding up, to size the array
duration = parseInt(roundUp(vid.duration,1));
// resize the array, defaulting entries to zero
console.log("resizing arrary to " + duration + " seconds.");
watched.resize(duration,0)
}
</script>
Related
I am attempting to make a simple timer (counting up from 0) with JavaScript and requestAnimationFrame. I want to start the timer from 0 when something is clicked. Currently my code displays the timer when the button is clicked, but it looks to me like requestAnimationFrame is running before the function is even called. If you load the code on a web page and wait a few seconds, then click the button, you will see the timer doesn't begin at 0, it starts at however many seconds it has been since the page first loaded. I'm at a loss and googling has not helped me figure out why/how the timer is starting counting before the function has been called.
My current code:
<div class="time">
Time: <label id="labelTime"></label>
</div>
<button id="button">Click me</button>
<script>
const button = document.getElementById('button');
button.addEventListener('click', clickButton);
function clickButton() {
runTimer();
}
function runTimer() {
let rAF_ID;
let rAFCallback = function(callback) {
let count = callback;
let s = Math.floor((count / 1000) % 60).toString().padStart(2, '0');
let m = Math.floor((count / 60000) % 60);
document.getElementById('labelTime').innerHTML = m + ":" + s;
rAF_ID = requestAnimationFrame(rAFCallback);
}
rAF_ID = requestAnimationFrame(rAFCallback);
}
</script>
The timestamp (DOMHighResTimeStamp) value passed into your rAFCallback function does not start from when the animation was first run, instead it has a "time origin" which varies on the context.
https://developer.mozilla.org/en-US/docs/Web/API/DOMHighResTimeStamp
If the script's global object is a Window, the time origin is determined as follows:
If the current Document is the first one loaded in the Window, the time origin is the time at which the browser context was created.
If during the process of unloading the previous document which was loaded in the window, a confirmation dialog was displayed to let the user confirm whether or not to leave the previous page, the time origin is the time at which the user confirmed that navigating to the new page was acceptable.
If neither of the above determines the time origin, then the time origin is the time at which the navigation responsible for creating the window's current Document took place.
If the script's global object is a WorkerGlobalScope (that is, the script is running as a web worker), the time origin is the moment at which the worker was created.
In all other cases, the time origin is undefined.
So if you want to get a delta-time value from when the animation was started, you'll need to do that yourself, like so:
let timestampAtStart = null;
let lastRequestId = null;
function myAnimateFunction( timestamp ) {
if( !timestampAtStart ) {
timestampAtStart = timestamp;
}
let timeSinceStart = timestamp - timestampAtStart;
console.log( timeSinceStart );
lastRequestId = window.requestAnimationFrame( myAnimateFunction );
}
function startAnimation() {
if( lastRequestId ) window.cancelAnimationFrame( lastRequestId );
timestampAtStart = null;
lastRequestId = window.requestAnimationFrame( myAnimateFunction );
}
I am trying to pause Google Maps markers from animating when elements pushed into an array reaches a certain number. I have tried to clearing the timer and resuming it however i am having some problems.
I have used console.log() to see where control is accessing however the only message i am getting from console.log() is '500 records saved'. The timer gets cleared however control does not access the else condition of the block to resume the timer.
I have also looked at the SO question Pause and play google map marker animation
However i am not using buttons to pause and play.
Here is the code any guidance will be greatly appreciated.
Console.log()
'500 records saved' and animation stops does not resume
var lastVertex = 1;
var stepnum=0;
var step = 90; //0.9 metres
var tick = 100; // milliseconds
var eol = [];
StartAnimation
function startAnimation(index) {
if (timerHandle[index]) clearTimeout(timerHandle[index]);
eol[index]=polyline[index].Distance();
map.setCenter(polyline[index].getPath().getAt(0));
poly2[index] = new google.maps.Polyline({path: [polyline[index].getPath().getAt(0)], strokeColor:"#FFFF00", strokeWeight:3});
timerHandle[index] = setTimeout("animate("+index+",50)",2000); // Allow time for the initial map display
}
Animate
function animate(index,d) {
if (d>eol[index]) {
marker[index].setPosition(endLocation[index].latlng);
if(marker[index].getPosition() == endLocation[index].latlng){
console.log('Completed'+' '+index);
}
return;
}
var p = polyline[index].GetPointAtDistance(d);
marker[index].setPosition(p);
updatePoly(index,d);
timerHandle[index] = setTimeout("animate("+index+","+(d+step)+")", tick);
citizens1.push({lat:marker[index].getPosition().lat(),lng:marker[index].getPosition().lng(),socialSecurityNumber:global_citizens[index].socialSecurityNumber});
if(citizens1.length = 500){
console.log('500 records saved');
window.clearTimeout( timerHandle[index]);
citizens1 = []; //clear the array
}
}
I seem to be having some unexpected results with a framerate counter in javascript. Up until recently the counter has been fine and I have been running my little js app at 30fps.
It uses setTimeout() (with a time adjustment to counter the system 'falling behind').
window.requestAnimFrame = (function()
{
return function (callback) {
time += FPS;
Heartbeat._eTime = (new Date().getTime() - Heartbeat._start);
var diff = Heartbeat._eTime - time;
Heartbeat._delta = FPS - diff;
Heartbeat._deltaS = Heartbeat._delta / 1000;
window.setTimeout(callback, FPS - diff);
};
})();
Heartbeat is merely an object that contains the frame rate info.
*Here is my problem: *
_MainLoopHandler: function () {
timer = new Date().getTime();
counter = timer;
while (this._messages.length > 0 && (counter - timer) < 5)
{
// process messages from _messages array
}
counter = new Date().getTime();
// THE ABOVE IS HAPPY AT 30 FPS
while ((counter - timer) < 6) {
1 + 1;
}
// THE ABOVE WHILE IS VERY UNHAPPY :(
}
So the above code block is the function that is called from setTimeout every 33.33 milliseconds (30 fps). if I take the bottom while loop out, the FPS counter will sit happily at 30fps. However, if I leave it in, the FPS counter goes crazy. it goes up to the 200FPS 300FPS then suddenly goes -200FPS -10FPS 0.01FPS. Its completely off the wall. The while loop will only run maybe 10 times per "frame".
Note also, the hard-coded values 5 and 6 are simply a check to see if 5 or 6 milliseconds have passed while processing the loops (for load balance).
Is this simply javascript being unable to handle the amount of info or has anyone else had a similar problem.
Thanks!
I don't really know what's going on, but I think you should use local variables to control your time, constantly reassess counter and process 1 message at a time. Also, I don't really understand that last loop (I've also renamed the variables):
_MainLoopHandler: function () {
var start = new Date().getTime();
var current;
do {
if (this._messages.length === 0) break;
// process 1 message
current = new Date().getTime();
} while (current - start < 5);
}
You can also encapsulate the timing concern in an object (not shown) to streamline the code:
_MainLoopHandler: function () {
var timing = new Timing();
do {
if (this._messages.length === 0) break;
// process 1 message
} while (timing.elapsed() < 5);
}
I'm trying to get my video (locally hosted, not streamed) to start after a certain time and stop after a certain duration. Someone here helped me out here with Javascript, but it's not working for me -- no effect on time of playback at all.
So, in my header, I've called the javascript like this:
<script src="Backend/build/Timer.js"></script>
And the actual javascript looks like this:
// JavaScript Document
var starttime = 2000; // start at 2 seconds
var endtime = 4000; // stop at 4 seconds
var video = document.getElementById('player1');
video.currentTime = starttime;
video.addEventListener("timeupdate", function() {
if (video.currentTime >= endtime) {
video.pause();
}
}, false);
Wrap your code into a function and invoke that function in document ready or body load event handler, otherwise video variable value may be invalid.
According to W3C standard:
The currentTime attribute must, on getting, return the current playback position, expressed in seconds. On setting, if the media element has a current media controller, then it must throw an INVALID_STATE_ERR exception; otherwise, the user agent must seek to the new value (which might raise an exception).
If you want to start playing the video at 2 seconds and stop at 4 seconds (in the video time stamp), set starttime, endtime to 2, 4 respectively, not 2000, 4000. Furthermore, before seeking to starttime, you must load the video resource once
function playVideo() {
var starttime = 2; // start at 2 seconds
var endtime = 4; // stop at 4 seconds
var video = document.getElementById('player1');
//handler should be bound first
video.addEventListener("timeupdate", function() {
if (this.currentTime >= endtime) {
this.pause();
}
}, false);
//suppose that video src has been already set properly
video.load();
video.play(); //must call this otherwise can't seek on some browsers, e.g. Firefox 4
try {
video.currentTime = starttime;
} catch (ex) {
//handle exceptions here
}
}
Can HTML5 <video> tag be played in reverse, or do I have to download 2 videos (forward and backward play)?
I'm looking for a solution that avoids a user from downloading 2 videos.
Without even going into HTML5 or Javascript, many video formats are streaming formats that are designed to be played forward. Playing it backwards would require decoding the whole stream, storing each raw frame on the disk to avoid clobbering memory, then rendering the frames backwards.
At least one person actually tried that using mplayer, though, so it can be done, at least in principle.
I managed to do it in an update method. Every frame I decrease video.currentTime to the time elapsed and so far it is the best result I managed to get.
aMediaElement.playbackRate = -1;
UAs may not support this, though it is valid to set playbackRate to a negative value.
This snippet just shows, how it could be done, but it takes some time to copy each frame. Which highly depends on the hardware.
It generates a canvas of each frame it has to play. When it's on 100% the playback starts directly and plays backward, forward... and so on. The original Video is also attached after the generation, but won't play automatically due to iframe rules.
It is fine for short videos and as a proof of concept.
Update:
I changed the order of the capture part so that the frame at max duration is skipped but the one at 0 is captured (forgot it before).
The frame at max duration caused a white canvas on every video i tried.
I also changed the playback to play it in reverse order as soon as the last frame is reached for an endless playback. So you easily see, that this playback is a bit CPU intensive compared to hardware accelerated videos.
fetch('https://i.imgur.com/BPQF5yy.mp4')
.then(res => res.blob())
.then(blob => {
return new Promise((res) => {
const fr = new FileReader();
fr.onload = e => res(fr.result);
fr.readAsDataURL(blob);
})
}).then(async(base64str) => {
const video = document.createElement("video");
video.src = base64str;
video.controls = true;
video.className = "w-50";
while (isNaN(video.duration))
await new Promise((r) => setTimeout(r, 50));
const FPS = 25;
const status = document.createElement("div"),
length = document.createElement("div");
length.innerText = `Generating ${Math.ceil(video.duration / (1 / FPS))} frames for a ${FPS} FPS playback (Video Duration = ${video.duration})`;
document.body.appendChild(length);
document.body.appendChild(status);
const frames = [],
copy = () => {
const c = document.createElement("canvas");
Object.assign(c, {
width: video.videoWidth,
height: video.videoHeight,
className: "w-50"
});
c.getContext("2d").drawImage(video, 0, 0);
return c;
};
// first seek outside of the loop this image won't be copied
video.currentTime = video.duration;
// this frame seems to be always white/transparent
while (video.currentTime) {
if (video.currentTime - 1 / FPS < 0)
video.currentTime = 0;
else
video.currentTime = video.currentTime - 1 / FPS;
await new Promise((next) => {
video.addEventListener('seeked', () => {
frames.push(copy());
status.innerText = (frames.length / (Math.ceil(video.duration / (1 / FPS))) * 100).toFixed(2) + '%';
next();
}, {
once: true
});
});
}
/*
* frames now contains all canvas elements created,
* I just append the first image and replace it on
* every tick with the next frame.
* using last.replaceWith(frames[currentPos]) guarantees a smooth playback.
*/
let i = 0, last = frames[0];
document.body.insertAdjacentHTML('beforeend', `<div class="w-50">Captured</div><div class="w-50">Video</div>`);
document.body.appendChild(frames[0]);
const interval = setInterval(() => {
if (frames[++i]) {
last.replaceWith(frames[i]);
last = frames[i];
} else {
frames.reverse();
i=0;
}
}, 1000 / FPS);
document.body.appendChild(video);
// won't :(
video.play();
});
/* Just for this example */
.w-50 {
width: 50%;
display: inline-block;
}
* {
margin: 0;
padding: 0;
font-family: Sans-Serif;
font-size: 12px;
}
I tried request animation frame, calculated the diff and updated currentTime. This does not work, the video tag doesn't repaint fast enough.
Get the HTMLMediaElement's duration then set an Interval that would run every second and playing the media by setting the .currentTime and decrease the value every second by 1. The media element must be fully downloaded for this to work. I've only tested this on 30-second clips and unsure if faster (lesser than 1sec.) intervals are possible. Note that this method still plays the media forward. Try increasing media playback rate to make it feel more seamless.