Safari does not stop downloading media after removing src and calling load() - javascript

After an HTML media element is paused, the browser keeps downloading the media.
MDN describes how the download can be stopped by removing the media element's src attribute and calling HTMLMediaElement.load(): MDN - Stopping the download of media.
In Safari, this is not sufficient to stop the media continuing to download. In the Web Inspector Network tab, the transfer can be seen to continue. This is despite the load() method being supported and the appropriate events being sent when it is called. (See MDN.)
Using the following example and monitoring the network, you should be able to see the behaviour in Safari differ compared to Chrome, for example.
<audio
controls preload="none" type="audio/mpeg"
src="http://bbcmedia.ic.llnwd.net/stream/bbcmedia_radio1_mf_p"
/>
<script>
const mediaEl = document.querySelector('audio');
mediaEl.addEventListener('abort', (e) => {
console.log('abort', e.target);
});
mediaEl.addEventListener('emptied', (e) => {
console.log('emptied', e.target);
});
mediaEl.addEventListener('pause', (e) => {
e.target.removeAttribute('src');
e.target.load();
});
</script>
Removing the media element from the DOM is not sufficient to stop the download either.
mediaEl.addEventListener('pause', (e) => {
e.target.parentNode.removeChild(e.target);
});
Is there a way to work around this issue in Safari and stop the download initiated by the media element?
My use case involves an audio stream. I do not want the browser to continue buffering after the user has stopped playback.

I had the same issue.
The only good workaround I've found is putting the audio tag in separate html and loading that into an iframe - then adding an event listener that reloads the audio tag's page when the stream is paused.

Related

How to overcome audio player issues in html5 - javascript applications? [duplicate]

With the release of OSX High-Sierra*, one of the new features in Safari is that videos on websites will not auto play anymore and scripts can't start it either, just like on iOS. As a user, I like the feature, but as a developer it puts a problem before me: I have an in-browser HTML5 game that contains video. The videos do not get automatically played anymore unless the user changes their settings. This messes up the game flow.
My question is, can I somehow use the players' interaction with the game as a trigger for the video to start playing automatically, even if said activity is not directly linked to the video element?
I cannot use jQuery or other frameworks, because of a restraint that my employer has put on our development. The one exception is pixi.js which - among all other animations - we are also using to play our videos inside a pixi container.
*The same restriction also applies on Mobile Chrome.
Yes, you can bind on event that are not directly ones triggered on the video element:
btn.onclick = e => vid.play();
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
So you can replace this button with any other splash screen requesting an user click, and you'll be granted access to play the video.
But to keep this ability, you must call at least once the video's play method inside the event handler itself.
Not working:
btn.onclick = e => {
// won't work, we're not in the event handler anymore
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Proper fix:
btn.onclick = e => {
vid.play().then(()=>vid.pause()); // grants full access to the video
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Ps: here is the list of trusted events as defined by the specs, I'm not sure if Safari limits itself to these, nor if it includes all of these.
Important note regarding Chrome and preparing multiple MediaElements
Chrome has a long-standing bug caused by the maximum simultaneous requests per host which does affect MediaElement playing in the page, limiting their number to 6.
This means that you can not use the method above to prepare more than 6 different MediaElements in your page.
At least two workarounds exist though:
It seems that once a MediaElement has been marked as user-approved, it will keep this state, even though you change its src. So you could prepare a maximum of MediaElements and then change their src when needed.
The Web Audio API, while also concerned by this user-gesture requirement can play any number of audio sources once allowed. So, thanks to the decodeAudioData() method, one could load all their audio resources as AudioBuffers, and even audio resources from videos medias, which images stream could just be displayed in a muted <video> element in parallel of the AudioBuffer.
In my case i was combining transparent video (with audio) with GSAP animation. The solution from Kaiido works perfectly!
First, on user interaction, start and pause the video:
videoPlayer.play().then(() => videoPlayer.pause());
After that you can play it whenever you want. Like this:
const tl = gsap.timeline();
tl.from('.element', {scale: 0, duration: 5);
tl.add(() => videoPlayer.play());
Video will play after the scale animation :).
Tested in Chrome, Safari on iPhone

Programmatically play video with sound on Safari and Mobile Chrome

With the release of OSX High-Sierra*, one of the new features in Safari is that videos on websites will not auto play anymore and scripts can't start it either, just like on iOS. As a user, I like the feature, but as a developer it puts a problem before me: I have an in-browser HTML5 game that contains video. The videos do not get automatically played anymore unless the user changes their settings. This messes up the game flow.
My question is, can I somehow use the players' interaction with the game as a trigger for the video to start playing automatically, even if said activity is not directly linked to the video element?
I cannot use jQuery or other frameworks, because of a restraint that my employer has put on our development. The one exception is pixi.js which - among all other animations - we are also using to play our videos inside a pixi container.
*The same restriction also applies on Mobile Chrome.
Yes, you can bind on event that are not directly ones triggered on the video element:
btn.onclick = e => vid.play();
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
So you can replace this button with any other splash screen requesting an user click, and you'll be granted access to play the video.
But to keep this ability, you must call at least once the video's play method inside the event handler itself.
Not working:
btn.onclick = e => {
// won't work, we're not in the event handler anymore
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Proper fix:
btn.onclick = e => {
vid.play().then(()=>vid.pause()); // grants full access to the video
setTimeout(()=> vid.play().catch(console.error), 5000);
}
<button id="btn">play</button><br>
<video id="vid" src="https://dl.dropboxusercontent.com/s/bch2j17v6ny4ako/movie720p.mp4"></video>
Ps: here is the list of trusted events as defined by the specs, I'm not sure if Safari limits itself to these, nor if it includes all of these.
Important note regarding Chrome and preparing multiple MediaElements
Chrome has a long-standing bug caused by the maximum simultaneous requests per host which does affect MediaElement playing in the page, limiting their number to 6.
This means that you can not use the method above to prepare more than 6 different MediaElements in your page.
At least two workarounds exist though:
It seems that once a MediaElement has been marked as user-approved, it will keep this state, even though you change its src. So you could prepare a maximum of MediaElements and then change their src when needed.
The Web Audio API, while also concerned by this user-gesture requirement can play any number of audio sources once allowed. So, thanks to the decodeAudioData() method, one could load all their audio resources as AudioBuffers, and even audio resources from videos medias, which images stream could just be displayed in a muted <video> element in parallel of the AudioBuffer.
In my case i was combining transparent video (with audio) with GSAP animation. The solution from Kaiido works perfectly!
First, on user interaction, start and pause the video:
videoPlayer.play().then(() => videoPlayer.pause());
After that you can play it whenever you want. Like this:
const tl = gsap.timeline();
tl.from('.element', {scale: 0, duration: 5);
tl.add(() => videoPlayer.play());
Video will play after the scale animation :).
Tested in Chrome, Safari on iPhone

Firefox doesn't recognize if video src gets interrupted

i'm currently playing around with video.js to make video watching a bit more reliable. The current case if: while watching a video, the connection gets interrupted (loss of internet connectivity).
With chrome it was no big deal: I created a function, attached the function to the error event and voila, if an error occurred, my function was able to recover (in this case its just trying to reload the video and seek to the last known position).
On firefox nothing happened at all. The error is never raised.
I tried to work around this a bit and noticed that firefox seems to think the file is fully loaded in case of a network interruption. So the bufferedPercent jumps to 1 and the loadedalldata event is also triggered. For me this seems to be just broken, but i'm unable to nail it down to a firefox or a video.js problem.
Anyone else having such problems or knows a better way to handle such problems?
Background info: to test the case, i run a HAProxy infront of two webservers running nginx who deliver the video files. To trigger my problem, i just kill the nginx which is currently delivering the stream. So a reconnect should work fine (as long as the other one is still working of course ;)
Thanks and regards,
Darkman
Could it be that you are binding the error on the video tag and not the source tag?
As stated here:
Instead of the error event being dispatched to the media element itself, it now gets delivered to the child elements corresponding to the sources resulting in the error.
<video controls id="videoTag" width="640" height="360" preload="auto">
<source src="pathto.mp4" type='video/mp4; codecs="avc1.42E01E, mp4a.40.2"' id="mp4Source">
<source src="pathto.webm" type='video/webm; codecs="vp8.0, vorbis"' id="webmSource">
</video>
<script type="text/javascript">
document.getElementById('mp4Source').addEventListener('error', function(ev) {
alert('mp4Source error');
}, false);
document.getElementById('webmSource').addEventListener('error', function(ev) {
alert('webmSource error');
}, false);
document.getElementById('videoTag').addEventListener('error', function(ev) {
alert('videoTag error');
}, false);
document.getElementById('videoTag').addEventListener('stalled', function(ev) {
alert('videoTag stalled');
}, false);
</script>
EDIT: also check for the stalled event as it triggers when one disconnects the network while the media resource is downloading. Source.
When detecting playback/network errors on HTML5 video I normally use something that follows the code at end of this section.
Thanks

Fully buffer video in Chrome

Is it possible to fully buffer HTML5 video in Chrome (and Opera)?
I host the movie in .mp4 and .webm on amazon S3. In HTML I use standard <video> tag. The server responds with status 206 Partial Content. It is great, as it allows the browser to download any part of the video but I need to be able to seek instantly to any part of the file.
I tried:
.PROGRESS event. When Chrome stops buffering the first part, the connection is killed. The next parts are downloaded in new connection, so JavaScript isn't able to continue checking total downloaded data. Even if it could, the old data isn't stored in cache. This method works in Firefox and Safari. Heck, even in IE10!
XHRS. I am able to fully download the movie, but when the video starts playing, Chrome tries to download the movie from the server again. I even tried to pass the movie into HTML in base64, but that's to much data
FileAPI. Chrome isn't able to create BLOB big enough and crashes.
Any help is welcome!
ps. I am aware of od and similar questions, but I hope something changed since they were asked.
Because S3 supports partial downloads, Chrome initially buffers "only what's needed" in front of the playhead and then stops buffering. It will continue buffering "only what's needed" when the video starts playing. This means that depending on the user's download speed it will stop buffering now and then, just because it has enough buffer to play continuously.
But if you pause the video after having played some, Chrome will not stop buffering and go through all the way to the end.
This example exploits that technique to entirely buffer the video off screen before showing it on the page.
Tested on Chrome 32
// Create video in background and start playing
var video = document.createElement('video');
video.src = 'video.mp4';
video.controls = true;
video.muted = true;
video.play();
// Pause immediately after it starts playing.
video.addEventListener("timeupdate", function() {
if (this.currentTime > 0) {
this.pause();
video.muted = false;
video.currentTime = 0
this.removeEventListener("timeupdate", arguments.callee, false);
// When whole video is buffered, show video on page
video.addEventListener("progress", function() {
if (Math.round(video.buffered.end(0)) / Math.round(video.seekable.end(0)) == 1) {
document.body.appendChild(video);
this.removeEventListener("progress", arguments.callee, false);
}
}, false);
}
}, false);
Have you tried the canplaythrough event?
Not in the traditional sense, I mean. Rather in a 'hacky' way. canplaythrough is triggered when the browser detects that it has enough video buffered to play continuously without pausing. I am guessing that this event triggers about the same time as chrome pauses buffering. If that's the case, it could be use to trigger a request for rest of the video.
I have not tried it, but the HTML5 video object contains a buffered property
var video = document.createElement('video');
video.buffered.start(0);
video.buffered.end(0);
Could you try monitoring the buffered (Such as with this thread chrome html5 video buffered.end event)
Then continually change the current time to right before the buffered time using
video.currentTime = video.buffered.end(0) - 1;
There are several ways to load a video all the way then play it:
1. There is the goody goody browser developer would be proud way:
var video = document.createElement('video');
video.src='myvideo.mp4';
document.appendChild(video);
video.load();
video.addEventListener('loadedmeta',function(){
video.play()
});
2. And there is the lazy "But, I'll get carpel tunnel!" developer way:
<video src='myvideo.mp4' onloadedmeta='this.play()' preload></video>
Sources:
http://www.w3schools.com/tags/tag_video.asp
how to run js code when video is fully buffered/loaded
How do I add new Video entirely from JavaScript?
javascript with html <video> load() & play() function not firing
http://www.w3.org/wiki/HTML/Elements/video
Be careful using video.buffered.end(0). According to my experience, it works with Chrome but it fails with IE9.
I don't know what happens, IE9 seems to forget to do the last update to video.buffered.end() at the end of the buffering process and the video.buffered.end(0) is a little bit smaller than video.duration.
So, if you think your page could be used with another browser than Chrome, don't use video.buffered.end(0).

Javascript onloadedmetadata event not firing on iOS devices

Part of my current project involves loading external videos through HTML5's native video tag and then resizing them with Javascript to be the full height & width of the DOM.
My code seems to work perfectly on desktop browsers, but when I load up my project on my ipad the video doesn't get resized because the onloadedmetadata event never gets fired.
Here is a small code sample which reproduces the problem:
function init() {
var video = document.getElementById('viddy');
video.addEventListener('loadedmetadata', function(e){
var dimensions = [video.videoWidth, video.videoHeight];
alert(dimensions);
});
}
document.addEventListener("DOMContentLoaded", init, false);
<video id="viddy" autoplay>
<source src="http://media.w3.org/2010/05/sintel/trailer.webm" type='video/webm' />
<source src="http://www.w3schools.com/html/movie.mp4" type="video/mp4" />
</video>
http://jsfiddle.net/AUSNu/213/
I've even tried coding up a solution using jQuery, on the off-chance that the event may fire, but it still doesn't.
$('#viddy').on('loadedmetadata', function() {
alert('test');
});
I even went as far as enabling remote debugging through safari on my ipad, but still no output within the console.
Are there any workarounds to this? I couldn't find much info about this on the web / in documentation.
Unfortunately, there isn't really a way around this. Mobile Safari will not download any part of the video file until it gets a user interaction (i.e. some kind of touch event), not even the header, which is required to know the dimensions.
In your specific example, you need to enable controls on the video so the user can start it playing. (Or you can write your own code that starts it, but it has to be triggered by a touch or click event.) Once it starts playing, the loadedmetadata even will fire, and you can do what you want.
I recommend reading this other answer where someone else was trying to do pretty much the same thing. It discusses the problem in more detail along with a working link. Also, it addresses another problem with scaling the video that you will probably run into.
Safari on iPad (iOS6) does not scale HTML5 video to fill 100% of page width

Categories

Resources