Related
I have created a site with image thumbnails of people I have photographed. When a visitor clicks on one of the thumbnails the full image is revealed using jQuery, and an audio introduction plays. I have a different audio introduction for each thumbnail/image combination - 15 at present with more being added daily.
I would like to ensure that if a visitor clicks on another thumbnail before the previous audio file has completed, that the previous audio file is stopped/paused to allow the new audio file to be played - thereby ensuring two or more tracks do not play simultaneously.
I am currently using the following snippet of code, wrapped in an anonymous function, to play each audio file individually when the appropriate thumbnail is clicked - so this snippet is duplicated for each audio file, but don't know how to ensure they do not play over one another.
$(".bridget-strevens").click(function(){
var audio = $('#bridget-strevens-intro')[0];
if (audio.paused){
audio.play();
} else {
audio.pause();
}
});
Any help you could give me would be very grateful, as I am just starting to learn jQuery, and don't have the knowledge to come up with a workable solution.
Thanks in advance for your help!
Add a .audio class to all your audio elements and loop through all of them when an audio is clicked.
$(".bridget-strevens").click(function () {
$('.audio').each(function (index, value) {
if (!value.paused) {
value.pause();
}
});
var audio = $('#bridget-strevens-intro')[0];
if (audio.paused) {
audio.play();
} else {
audio.pause();
}
});
If that seems too heavy for you then simply add the audio element in a global variable such as:
var currentAudio;
Then when a new audio is clicked, simply pause that one, play the new one and update the currentAudio variable with the new element currently being played.
var currentAudio = null;
$(".bridget-strevens").click(function () {
if(currentAudio != null && !currentAudio.paused){
currentAudio.pause();
}
var audio = $('#bridget-strevens-intro')[0];
if (audio.paused) {
audio.play();
currentAudio = audio;
} else {
audio.pause();
}
});
Update:
Thanks for the prompt responses! Grimbode, I've tried what you
suggest, and that seems to work. However is there the ability to stop
and reset rather than just pause - so if they clicked on 1 then [2]
before 1 finished, then clicked 1 again, that 1 would start from
the beginning again rather than the point at which it was paused? And
is there any way of check the state 'globally', and then add code for
each individual audio file - just to keep the amount of code and
duplication down? Thanks again!! –
Yes. Play audio and restart it onclick explains in detail how to do this. The final result would look something like this:
var currentAudio = null;
$(".bridget-strevens").click(function () {
if(currentAudio != null && !currentAudio.paused && currentAudio != this){
currentAudio.pause();
//Here we reset the audio and put it back to 0.
currentAudio.currentTime = 0;
}
var audio = $('#bridget-strevens-intro')[0];
if (audio.paused) {
audio.play();
currentAudio = audio;
} else {
audio.pause();
}
});
You can't really optimize the code much more. You're going to have apply the click event on every audio element. You're going to have to keep the current playing audio element memorized so you don't have to loop through all the audio files.
If you really want to take this further you could create a library to handle everything. Here is an example:
(function(){
var _ = function(o){
if(!(this instanceof _)){
return new _(o);
}
if(typeof o === 'undefined'){
o = {};
}
//here you set attributes
this.targets = o.targets || {};
this.current = o.current || null;
};
//create fn shortcut
_.fn = _.prototype = {
init: function(){}
}
//Here you create your methods
_.fn.load = function(){
//here you load all the files in your this.targets.. meaning you load the source
//OR you add the click events on them.
//returning this for chainability
return this
};
//exporting
window._ = _;
})();
//here is how you use it
_({
targets: $('.audio')
}).load();
I am using video.js (http://www.videojs.com/) to build a video approval system and need to log user actions in the player. I can do this easily enough with play, pause, end etc. but have hit a problem when trying to log seeks.
I want to be able to log the start and end times of any seeks within the plaback, so we know if the user has not actually watched a section of the video. The player seems to offer events to support this, but I am struggling to get correct timings from it.
When the user skips through a video the player emits the following events in order: pause, seeking, seeked, play.
If I query the player object at any of these events using currentTime() the result is always the end time for the seek, even on the initial pause event. This means I can log where the seek ended but not where it started.
Can anyone help me to find the position in the video where the seek begins?
If this is not possible, I'd settle for a way to disable seeking during playback.
EDIT: adding code as requested. It's pretty simple:
var trackedPlayer = videojs('pvp-player');
trackedPlayer.on("play", function (e) {
console.log("Video playback started: " + trackedPlayer.currentTime());
});
trackedPlayer.on("pause", function (e) {
console.log("Video playback paused: " + trackedPlayer.currentTime());
});
trackedPlayer.on("seeking", function (e) {
console.log("Video seeking: " + trackedPlayer.currentTime());
});
trackedPlayer.on("seeked", function (e) {
console.log("Video seek ended: " + trackedPlayer.currentTime());
});
trackedPlayer.on("ended", function (e) {
console.log("Video playback ended.");
});
If I can get all the tracking I want I will replace console.log with ajax calls to store the data.
You can listen to timeupdate und take the next to last value you got there before seeking is called as your source:
var previousTime = 0;
var currentTime = 0;
trackedPlayer.on('timeupdate', function() {
previousTime = currentTime;
currentTime = trackedPlayer.currentTime();
});
trackedPlayer.on('seeking', function() {
console.log('seeking from', previousTime, 'to', currentTime, '; delta:', currentTime - previousTime);
});
This seems to work with the HTML5 tech. I have not tested with other techs.
There is, however, one glitch: the first time seeking a paused player yields only a small delta (and the almost-same previous value for both variables). But this shouldn’t matter much since the delta is only a few hundred milliseconds (and I gather you’re only interested in the “from” value).
Update
seeked is triggered far more infrequently than seeking. Try the following.
var previousTime = 0;
var currentTime = 0;
var seekStart = null;
trackedPlayer.on('timeupdate', function() {
previousTime = currentTime;
currentTime = trackedPlayer.currentTime();
});
trackedPlayer.on('seeking', function() {
if(seekStart === null) {
seekStart = previousTime;
}
});
trackedPlayer.on('seeked', function() {
console.log('seeked from', seekStart, 'to', currentTime, '; delta:', currentTime - previousTime);
seekStart = null;
});
There are also many libraries for debouncing function calls (in this case the call to your backend).
I needed to find the same value for a project I was working on so I could determine whether or not a user was skipping forward or backward in a videojs player.
Initially, I thought to save the currentTime() a user was seeking from on timeupdate then immediately removing my timeupdate listener once seeking was dispatched. While this worked in some browsers like Chrome, unfortunately, I found that other browsers continued to fire timeupdate more frequently and would continue to update the currentTime() I was saving after the player actually seeked.
Here was the solution that ultimately worked across Safari/Chrome/Firefox. I have yet to test in IE.
var previousTime = 0,
currentTime = 0,
completeTime = 0,
position = 0;
trackedPlayer.on('timeupdate', function() {
previousTime = currentTime;
currentTime = Math.floor(player.currentTime());
// save 'position' so long as time is moving forward with each update
if (previousTime < currentTime) {
position = previousTime;
previousTime = currentTime;
}
});
// when seeking starts
trackedPlayer.on('seeking', function() {
player.off('timeupdate', onTimeUpdate);
player.one('seeked', onSeekComplete);
});
// when seeking completes
trackedPlayer.on('seeked', function() {
completeTime = Math.floor(player.currentTime());
console.log("User came from: " + position);
console.log("User ended at: " + completeTime);
});
I know this is an old post but this is the only solution that worked for me.
var counter = 0;
var beforeTimeChange = 0;
function handleSeeking() {
var timeoutTime = 300;
var beforeCounter = counter + 1;
if (trackedPlayer.cache_.currentTime === trackedPlayer.duration()) {
return;
// when video starts over, calls seek
}
beforeTimeChange = beforeTimeChange || trackedPlayer.cache_.currentTime;
setTimeout(function() {
if (beforeCounter === counter) {
console.log('before seek', beforeTimeChange, '\nafter seek', trackedPlayer.currentTime() - (timeoutTime / 1000));
counter = 0;
beforeTimeChange = 0;
}
}, timeoutTime);
counter++;
}
trackedPlayer.on('seeking', handleSeeking);
For a more accurate solution, you can listen to the events that trigger the seek such as mousedown on progress bar, left key, right key etc., and get the current time from these events.
For example in version 7.10.2 you can do the following,
let seekStartTime;
player.controlBar.progressControl.on('mousedown', () => seekStartTime = player.currentTime());
player.controlBar.progressControl.seekBar.on('mousedown', () => seekStartTime = player.currentTime());
player.on('keydown', (e) => {
if (e.key === "ArrowLeft" || e.key === "ArrowRight") {
seekStartTime = player.currentTime();
}
});
console.log(seekStartTime);
Note 1: There are two seperate mousedown event listeners for progress control and seek bar. This is because the video can be seeked by clicking outside the seek bar on the progress control as well.
Note 2: Seeking using hotkey numbers does not pause the video. However, if necessary you can add those keydown listeners too.
I needed to find the start and end of a seeking action in my project and I used #Motorcykey answer and it worked, but there was a small bug. when I tried to seek to a time before the current time while the player was paused, the position didn't get updated. so I added just one line and it fixed it. I've tried other solutions too but so far this was the best solution that I've found. Here's the code snippet on player 'seeked'
player.on('seeked', function () {
completeTime = Math.floor(player.currentTime());
console.log("User came from: " + position);
console.log("User ended at: " + completeTime);
position= Math.floor(player.currentTime())
});
Try with this code to know the length of video.
var duration = document.getElementById("duration");
var vid_duration = Math.round(document.getElementById("video").duration);
//alert(vid_duration);
duration.innerHTML = vid_duration;
//duration.firstChild.nodeValue = vid_duration;
I am trying to play list of video files stored in a folder one by one but if any of the file is missing or deleted the program will stop but i want it jump to the next video and continue its execution is there any way to catch this error and jump to the next one?
function advanceVideo()
{
video_count++;
if (video_count > num_files) video_count = 1;
videoPlayer.setAttribute("src","video/video"+video_count+".ogg");
video.load();
video.play();
}
I am using the above function for accessing the video file in the folder.
Make use of the various events the <video /> element offers
// function to load the next video (without .play())
function advanceVideo() {
video_count = (video_count + 1) % num_files;
this.setAttribute("src", "video/video" + video_count + ".ogg");
this.load();
}
// if the current video has reached the end, load the next
videoPlayer.addEventListener("ended", advanceVideo);
// if the browser fetched enough data to play the video start playing
videoPlayer.addEventListener("canplay", function() {
this.play();
});
// if an error occured load the next video
videoPlayer.addEventListener("error", advanceVideo);
Update
This should do what you've requested in your comment
var num_files = 2,
playedVideos = 0;
// load next video
function advanceVideo() {
// current video id or -1 if not found
var curId = parseInt((/video(\d+)\.ogg$/.exec(this.src) || [, -1])[1], 10);
// reset if all videos have been played
if (playedVideos == num_files) {
playedVideos = 0;
curId = -1;
}
this.src = "video/video" + (curId + 1) + ".ogg";
this.load();
}
// if the current video has reached the end, load the next
videoPlayer.addEventListener("ended", function() {
playedVideos++;
advanceVideo();
});
// if the browser fetched enough data to play the video start playing
videoPlayer.addEventListener("canplay", function() {
this.play();
});
// if an error occured load the next video
videoPlayer.addEventListener("error", advanceVideo);
If there are less videos to play than num_files indicates, this will end in an "infinite loop" as it will try all videos until curId reaches the value Infinity!
Try using try-catch block:
function advanceVideo()
{
video_count++;
if (video_count > num_files) video_count = 1;
videoPlayer.setAttribute("src","video/video"+video_count+".ogg");
try {
video.load();
video.play();
} catch (e) { }
}
I'm trying to accomplish a simple doodle-like behaviour, where a mp3/ogg sound rings on click, using the html tag. It is supposed to work under Firefox, Safari and Safari iPad is very desireable.
I've tried many approaches and have come down to this:
HTML
<span id="play-blue-note" class="play blue" ></span>
<span id="play-green-note" class="play green" ></span>
<audio id="blue-note" style="display:none" controls preload="auto" autobuffer>
<source src="blue.mp3" />
<source src="blue.ogg" />
<!-- now include flash fall back -->
</audio>
<audio id="green-note" style="display:none" controls preload="auto" autobuffer>
<source src="green.mp3" />
<source src="green.ogg" />
</audio>
JS
function addSource(elem, path) {
$('<source>').attr('src', path).appendTo(elem);
}
$(document).ready(function() {
$('body').delegate('.play', 'click touchstart', function() {
var clicked = $(this).attr('id').split('-')[1];
$('#' + clicked + '-note').get(0).play();
});
});
This seems to work great under Firefox but Safari seems to have a delay whenever you click, even when you click several times and the audio file has loaded. On Safari on iPad it behaves almost unpredictably.
Also, Safari's performance seems to improve when I test locally, I'm guessing Safari is downloading the file each time. Is this possible? How can I avoid this?
Thanks!
On desktop Safari, adding AudioContext fixes the issue:
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioCtx = new AudioContext();
I found out by accident, so I have no idea why it works, but this removed the delay on my app.
I just answered another iOS/<audio> question a few minutes ago. Seems to apply here as well:
Preloading <audio> and <video> on iOS devices is disabled to save bandwidth.
In Safari on iOS (for all devices, including iPad), where the user may
be on a cellular network and be charged per data unit, preload and
autoplay are disabled. No data is loaded until the user initiates it.
Source: Safari Developer Library
The problem with Safari is that it puts a request every time for the audio file being played. You can try creating an HTML5 cache manifest. Unfortunately my experience has been that you can only add to the cache one audio file at a time. A workaround might be to merge all your audio files sequentially into a single audio file, and start playing at a specific position depending on the sound needed. You can create an interval to track the current play position and pause it once it has reached a certain time stamp.
Read more about creating an HTML5 cache manifest here:
http://www.html5rocks.com/en/tutorials/appcache/beginner/
http://www.whatwg.org/specs/web-apps/current-work/multipage/offline.html
Hope it helps!
HTML5 Audio Delay on Safari iOS (<audio> Element vs AudioContext)
Yes, Safari iOS has an audio delay when using the native <audio> Element ...however this can be overcome by using AudioContext.
My code snippet is based on what I learnt from https://lowlag.alienbill.com/
Please test the functionality on your own iOS device (I tested in iOS 12)
https://fiddle.jshell.net/eLya8fxb/51/show/
Snippet from JS Fiddle
https://jsfiddle.net/eLya8fxb/51/
// Requires jQuery
// Adding:
// Strip down lowLag.js so it only supports audioContext (So no IE11 support (only Edge))
// Add "loop" monkey patch needed for looping audio (my primary usage)
// Add single audio channel - to avoid overlapping audio playback
// Original source: https://lowlag.alienbill.com/lowLag.js
if (!window.console) console = {
log: function() {}
};
var lowLag = new function() {
this.someVariable = undefined;
this.showNeedInit = function() {
lowLag.msg("lowLag: you must call lowLag.init() first!");
}
this.load = this.showNeedInit;
this.play = this.showNeedInit;
this.pause = this.showNeedInit;
this.stop = this.showNeedInit;
this.switch = this.showNeedInit;
this.change = this.showNeedInit;
this.audioContext = undefined;
this.audioContextPendingRequest = {};
this.audioBuffers = {};
this.audioBufferSources = {};
this.currentTag = undefined;
this.currentPlayingTag = undefined;
this.init = function() {
this.msg("init audioContext");
this.load = this.loadSoundAudioContext;
this.play = this.playSoundAudioContext;
this.pause = this.pauseSoundAudioContext;
this.stop = this.stopSoundAudioContext;
this.switch = this.switchSoundAudioContext;
this.change = this.changeSoundAudioContext;
if (!this.audioContext) {
this.audioContext = new(window.AudioContext || window.webkitAudioContext)();
}
}
//we'll use the tag they hand us, or else the url as the tag if it's a single tag,
//or the first url
this.getTagFromURL = function(url, tag) {
if (tag != undefined) return tag;
return lowLag.getSingleURL(url);
}
this.getSingleURL = function(urls) {
if (typeof(urls) == "string") return urls;
return urls[0];
}
//coerce to be an array
this.getURLArray = function(urls) {
if (typeof(urls) == "string") return [urls];
return urls;
}
this.loadSoundAudioContext = function(urls, tag) {
var url = lowLag.getSingleURL(urls);
tag = lowLag.getTagFromURL(urls, tag);
lowLag.msg('webkit/chrome audio loading ' + url + ' as tag ' + tag);
var request = new XMLHttpRequest();
request.open('GET', url, true);
request.responseType = 'arraybuffer';
// Decode asynchronously
request.onload = function() {
// if you want "successLoadAudioFile" to only be called one time, you could try just using Promises (the newer return value for decodeAudioData)
// Ref: https://developer.mozilla.org/en-US/docs/Web/API/BaseAudioContext/decodeAudioData
//Older callback syntax:
//baseAudioContext.decodeAudioData(ArrayBuffer, successCallback, errorCallback);
//Newer promise-based syntax:
//Promise<decodedData> baseAudioContext.decodeAudioData(ArrayBuffer);
// ... however you might want to use a pollfil for browsers that support Promises, but does not yet support decodeAudioData returning a Promise.
// Ref: https://github.com/mohayonao/promise-decode-audio-data
// Ref: https://caniuse.com/#search=Promise
// var retVal = lowLag.audioContext.decodeAudioData(request.response);
// Note: "successLoadAudioFile" is called twice. Once for legacy syntax (success callback), and once for newer syntax (Promise)
var retVal = lowLag.audioContext.decodeAudioData(request.response, successLoadAudioFile, errorLoadAudioFile);
//Newer versions of audioContext return a promise, which could throw a DOMException
if (retVal && typeof retVal.then == 'function') {
retVal.then(successLoadAudioFile).catch(function(e) {
errorLoadAudioFile(e);
urls.shift(); //remove the first url from the array
if (urls.length > 0) {
lowLag.loadSoundAudioContext(urls, tag); //try the next url
}
});
}
};
request.send();
function successLoadAudioFile(buffer) {
lowLag.audioBuffers[tag] = buffer;
if (lowLag.audioContextPendingRequest[tag]) { //a request might have come in, try playing it now
lowLag.playSoundAudioContext(tag);
}
}
function errorLoadAudioFile(e) {
lowLag.msg("Error loading webkit/chrome audio: " + e);
}
}
this.playSoundAudioContext = function(tag) {
var context = lowLag.audioContext;
// if some audio is currently active and hasn't been switched, or you are explicitly asking to play audio that is already active... then see if it needs to be unpaused
// ... if you've switch audio, or are explicitly asking to play new audio (that is not the currently active audio) then skip trying to unpause the audio
if ((lowLag.currentPlayingTag && lowLag.currentTag && lowLag.currentPlayingTag === lowLag.currentTag) || (tag && lowLag.currentPlayingTag && lowLag.currentPlayingTag === tag)) {
// find currently paused audio (suspended) and unpause it (resume)
if (context !== undefined) {
// ref: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/suspend
if (context.state === 'suspended') {
context.resume().then(function() {
lowLag.msg("playSoundAudioContext resume " + lowLag.currentPlayingTag);
return;
}).catch(function(e) {
lowLag.msg("playSoundAudioContext resume error for " + lowLag.currentPlayingTag + ". Error: " + e);
});
return;
}
}
}
if (tag === undefined) {
tag = lowLag.currentTag;
}
if (lowLag.currentPlayingTag && lowLag.currentPlayingTag === tag) {
// ignore request to play same sound a second time - it's already playing
lowLag.msg("playSoundAudioContext already playing " + tag);
return;
} else {
lowLag.msg("playSoundAudioContext " + tag);
}
var buffer = lowLag.audioBuffers[tag];
if (buffer === undefined) { //possibly not loaded; put in a request to play onload
lowLag.audioContextPendingRequest[tag] = true;
lowLag.msg("playSoundAudioContext pending request " + tag);
return;
}
// need to create a new AudioBufferSourceNode every time...
// you can't call start() on an AudioBufferSourceNode more than once. They're one-time-use only.
var source;
source = context.createBufferSource(); // creates a sound source
source.buffer = buffer; // tell the source which sound to play
source.connect(context.destination); // connect the source to the context's destination (the speakers)
source.loop = true;
lowLag.audioBufferSources[tag] = source;
// find current playing audio and stop it
var sourceOld = lowLag.currentPlayingTag ? lowLag.audioBufferSources[lowLag.currentPlayingTag] : undefined;
if (sourceOld !== undefined) {
if (typeof(sourceOld.noteOff) == "function") {
sourceOld.noteOff(0);
} else {
sourceOld.stop();
}
lowLag.msg("playSoundAudioContext stopped " + lowLag.currentPlayingTag);
lowLag.audioBufferSources[lowLag.currentPlayingTag] = undefined;
lowLag.currentPlayingTag = undefined;
}
// play the new source audio
if (typeof(source.noteOn) == "function") {
source.noteOn(0);
} else {
source.start();
}
lowLag.currentTag = tag;
lowLag.currentPlayingTag = tag;
if (context.state === 'running') {
lowLag.msg("playSoundAudioContext started " + tag);
} else if (context.state === 'suspended') {
/// if the audio context is in a suspended state then unpause (resume)
context.resume().then(function() {
lowLag.msg("playSoundAudioContext started and then resumed " + tag);
}).catch(function(e) {
lowLag.msg("playSoundAudioContext started and then had a resuming error for " + tag + ". Error: " + e);
});
} else if (context.state === 'closed') {
// ignore request to pause sound - it's already closed
lowLag.msg("playSoundAudioContext failed to start, context closed for " + tag);
} else {
lowLag.msg("playSoundAudioContext unknown AudioContext.state for " + tag + ". State: " + context.state);
}
}
this.pauseSoundAudioContext = function() {
// not passing in a "tag" parameter because we are playing all audio in one channel
var tag = lowLag.currentPlayingTag;
var context = lowLag.audioContext;
if (tag === undefined) {
// ignore request to pause sound as nothing is currently playing
lowLag.msg("pauseSoundAudioContext nothing to pause");
return;
}
// find currently playing (running) audio and pause it (suspend)
if (context !== undefined) {
// ref: https://developer.mozilla.org/en-US/docs/Web/API/AudioContext/suspend
if (context.state === 'running') {
lowLag.msg("pauseSoundAudioContext " + tag);
context.suspend().then(function() {
lowLag.msg("pauseSoundAudioContext suspended " + tag);
}).catch(function(e) {
lowLag.msg("pauseSoundAudioContext suspend error for " + tag + ". Error: " + e);
});
} else if (context.state === 'suspended') {
// ignore request to pause sound - it's already suspended
lowLag.msg("pauseSoundAudioContext already suspended " + tag);
} else if (context.state === 'closed') {
// ignore request to pause sound - it's already closed
lowLag.msg("pauseSoundAudioContext already closed " + tag);
} else {
lowLag.msg("pauseSoundAudioContext unknown AudioContext.state for " + tag + ". State: " + context.state);
}
}
}
this.stopSoundAudioContext = function() {
// not passing in a "tag" parameter because we are playing all audio in one channel
var tag = lowLag.currentPlayingTag;
if (tag === undefined) {
// ignore request to stop sound as nothing is currently playing
lowLag.msg("stopSoundAudioContext nothing to stop");
return;
} else {
lowLag.msg("stopSoundAudioContext " + tag);
}
// find current playing audio and stop it
var source = lowLag.audioBufferSources[tag];
if (source !== undefined) {
if (typeof(source.noteOff) == "function") {
source.noteOff(0);
} else {
source.stop();
}
lowLag.msg("stopSoundAudioContext stopped " + tag);
lowLag.audioBufferSources[tag] = undefined;
lowLag.currentPlayingTag = undefined;
}
}
this.switchSoundAudioContext = function(autoplay) {
lowLag.msg("switchSoundAudioContext " + (autoplay ? 'and autoplay' : 'and do not autoplay'));
if (lowLag.currentTag && lowLag.currentTag == 'audio1') {
lowLag.currentTag = 'audio2';
} else {
lowLag.currentTag = 'audio1';
}
if (autoplay) {
lowLag.playSoundAudioContext();
}
}
this.changeSoundAudioContext = function(tag, autoplay) {
lowLag.msg("changeSoundAudioContext to tag " + tag + " " + (autoplay ? 'and autoplay' : 'and do not autoplay'));
if(tag === undefined) {
lowLag.msg("changeSoundAudioContext tag is undefined");
return;
}
lowLag.currentTag = tag;
if (autoplay) {
lowLag.playSoundAudioContext();
}
}
this.msg = function(m) {
m = "-- lowLag " + m;
console.log(m);
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.8.0/jquery.min.js"></script>
<script>
// AudioContext
$(document).ready(function() {
lowLag.init();
lowLag.load(['https://coubsecure-s.akamaihd.net/get/b86/p/coub/simple/cw_looped_audio/f0dab49f867/083bf409a75db824122cf/med_1550250381_med.mp3'], 'audio1');
lowLag.load(['https://coubsecure-s.akamaihd.net/get/b173/p/coub/simple/cw_looped_audio/0d5adfff2ee/80432a356484068bb0e15/med_1550254045_med.mp3'], 'audio2');
// starts with audio1
lowLag.changeSoundAudioContext('audio1', false);
});
// ----------------
// Audio Element
$(document).ready(function() {
var $audioElement = $('#audioElement');
var audioEl = $audioElement[0];
var audioSources = {
"audio1": "https://coubsecure-s.akamaihd.net/get/b86/p/coub/simple/cw_looped_audio/f0dab49f867/083bf409a75db824122cf/med_1550250381_med.mp3",
"audio2": "https://coubsecure-s.akamaihd.net/get/b173/p/coub/simple/cw_looped_audio/0d5adfff2ee/80432a356484068bb0e15/med_1550254045_med.mp3"
};
playAudioElement = function() {
audioEl.play();
}
pauseAudioElement = function() {
audioEl.pause();
}
stopAudioElement = function() {
audioEl.pause();
audioEl.currentTime = 0;
}
switchAudioElement = function(autoplay) {
var source = $audioElement.attr('data-source');
if (source && source == 'audio1') {
$audioElement.attr('src', audioSources.audio2);
$audioElement.attr('data-source', 'audio2');
} else {
$audioElement.attr('src', audioSources.audio1);
$audioElement.attr('data-source', 'audio1');
}
if (autoplay) {
audioEl.play();
}
}
changeAudioElement = function(tag, autoplay) {
var source = $audioElement.attr('data-source');
if(tag === undefined || audioSources[tag] === undefined) {
return;
}
$audioElement.attr('src', audioSources[tag]);
$audioElement.attr('data-source', tag);
if (autoplay) {
audioEl.play();
}
}
changeAudioElement('audio1', false); // starts with audio1
});
</script>
<h1>
AudioContext (api)
</h1>
<button onClick="lowLag.play();">Play</button>
<button onClick="lowLag.pause();">Pause</button>
<button onClick="lowLag.stop();">Stop</button>
<button onClick="lowLag.switch(true);">Swtich</button>
<button onClick="lowLag.change('audio1', true);">Play 1</button>
<button onClick="lowLag.change('audio2', true);">Play 2</button>
<hr>
<h1>
Audio Element (api)
</h1>
<audio id="audioElement" controls loop preload="auto" src="">
</audio>
<br>
<button onClick="playAudioElement();">Play</button>
<button onClick="pauseAudioElement();">Pause</button>
<button onClick="stopAudioElement();">Stop</button>
<button onClick="switchAudioElement(true);">Switch</button>
<button onClick="changeAudioElement('audio1', true);">Play 1</button>
<button onClick="changeAudioElement('audio2', true);">Play 2</button>
Apple decided (to save money on celluar) to not pre-load <audio> and <video> HTML elements.
From the Safari Developer Library:
In Safari on iOS (for all devices, including iPad), where the user may
be on a cellular network and be charged per data unit, preload and
autoplay are disabled. No data is loaded until the user initiates it.
This means the JavaScript play() and load() methods are also inactive
until the user initiates playback, unless the play() or load() method
is triggered by user action. In other words, a user-initiated Play
button works, but an onLoad="play()" event does not.
This plays the movie: <input type="button" value="Play" onClick="document.myMovie.play()">
This does nothing on iOS: <body onLoad="document.myMovie.play()">
I don't think you can bypass this restriction, but you might be able to.
Remember: Google is your best friend.
Update: After some experimenting, I found a way to play the <audio> with JavaScript:
var vid = document.createElement("iframe");
vid.setAttribute('src', "http://yoursite.com/yourvideooraudio.mp4"); // replace with actual source
vid.setAttribute('width', '1px');
vid.setAttribute('height', '1px');
vid.setAttribute('scrolling', 'no');
vid.style.border = "0px";
document.body.appendChild(vid);
Note: I only tried with <audio>.
Update 2: jsFiddle here. Seems to work.
Unfortunately, the only way to make it work properly in Safari we need to use WebAudio API, or third-party libs to handle this. Check the source code here (it's not minified)
https://drums-set-js.herokuapp.com/index.html
https://drums-set-js.herokuapp.com/app.js
Same issue. I tried to preload it via different ways. Finally I wrapped animation logic to "playing" callback. So this logic should work only if file loaded and playing started, but as a result I see that animation logic already started, and audio playing with around 2 seconds delay.
It's braking my mind, how it can has delay if audio already called "playing" callback?
Audio Context resolved my issue.
The simplest example I found here
https://developer.mozilla.org/en-US/docs/Web/API/Body/arrayBuffer
getData - preparing your audio file;
then you can play it with source.start(0);
This link missed how to get audioCtx you can copy it here
let audioCtx = new (window.AudioContext || window.webkitAudioContext)();
your audio files are loaded once then cached.. playing the sounds repeatedly, even after page refresh, did not cause further HTTP requests in Safari..
i just had a look at one of your sounds in an audio editor - there was a small amount of silence at the beginning of the file.. this will manifest as latency..
is the Web Audio API a viable option for you?
I am having this same issue. What is odd is that I am preloading the file. But with WiFi it plays fine, but on phone data, there is a long delay before starting. I thought that had something to do with load speeds, but I do not start playing my scene until all images and the audio file are loaded. Any suggestions would be great. (I know this isn't an answer but I thought it better that making a dup post).
I would simply create <audio autoplay /> dom element on click, this works in all major browsers - no need to handle events and trigger play manually
if you want to respond to audio status change manually - I would suggest to listen for play event instead of loadeddata - it's behavior is more consistent in different browsers
If you have a small/short audio file that doesn't require a lot of audio clarity, you can convert the audio file to base64 encoding.
This way the audio file will be text based and doesn't have latency related to downloading the audio file, since iOS downloads the audio pretty much when it's played.
On one hand, it's nice what iOS does to prevent abuse. On the other hand, it's annoying when it gets in the way of legitimate usage.
Here's a base64 encoder for audio files.
I have this audio tag playing in the background, and I'm able to store the progress in seconds to a cookie.
But in no way I'm able to start the audio from that cookie. (for continuing on other pages)
$("p#sound audio").currentTime = $.cookie("audioTime");
<audio autoplay="autoplay" loop="loop" ontimeupdate="document.getElementById('tracktime').innerHTML = Math.floor(this.currentTime); $.cookie('audioTime', Math.floor(this.currentTime));">
<source src="audio/song.ogg" type="audio/ogg" />
<source src="audio/song.mp3" type="audio/mp3" />
Your browser does not support the audio tag.
</audio>
<span id="tracktime">0</span>
Does this have to do with the song being loaded again from start?
Thanks!
EDIT:
$("p#sound audio").get[0].currentTime
With .get[0], it doesn't work either.
Can someone please clear things up for me? Greatly appreciated!
You need to wait until audio source loads before you set the current time.
$(function(){
$('audio').bind('canplay', function(){
$(this)[0].currentTime = $.cookie('audioTime');
});
});
You can set the start time by adding t=<time> to the URL, as documented here: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Using_HTML5_audio_and_video#Specifying_playback_range
E.g. <audio src="http://example.com/audio.mp3#t=50></audio>
At first there is an error in your code because currentTime is not a part of jQuery (but you already know this)
$("p#sound audio").currentTime // is incorrect (refers to a property of jQuery)
$("p#sound audio")[0].currentTime // is correct (refers to a property of DOM element)
I discover that the audio tag has some strange things and can be operate differently from browser to browser, for example in Chrome.
At first you have to wait for the 'durationchange' event to be sure the length is known by the object.
After this you have to start the stream with 'play()' (if not already started) and pause it (sometimes after a short delay) with the 'pause()' function. Then you can change the 'currentTime' property with the value. After this you have to start the stream again by using the 'play()' function.
Also it is sometimes needed to load the stream by yourself by using the 'load()' function.
Something like this:
$(document).ready( function()
{
var a = $('audio:first'),
o = a[0];
a.on( 'durationchange', function(e)
{
var o = e.target;
if( o )
{
o.pause();
o.currentTime = parseInt( $.cookie("audioTime"));
o.play();
}
});
if( o )
{
o.load();
o.play();
}
});
You have to play with it to be sure what is the best in your situation, for example the resume (play again) method to delay it for a second or so.
When using this method you don't have to use the autoplay feature because most of the time it doesn't work.
Hope it helps, greetz,
Erwinus
what I found in my case is that there is an issue with context somewhere. I initialize audio under the window context but when I try to change currentTime from XMLHttpRequest response it does NOT work. I don't know the answer yet but I'm providing a clue maybe an expert in Javascript will know how to make it work.
/* initialize this.audio player */
Audio = function() {
var that = this;
// keep track of playback status
var AudioStatus = {
isPlaying : false
};
// define references to this.audio, pulldown menu, play-button, slider and time display
that.audio = document.querySelector("AUDIO");
/* load track by menu-index */
var loadTrack = function() {
if(that.audio == null){
log("audio null"); return;
}
that.audio.src = '../sounds/400.mp3';
that.audio.load();
};
/* callback to play or pause */
that._play = function() {
if(that.audio == null){
log("audio null"); return;
}
that.audio.play();
AudioStatus.isPlaying = true;
};
that._pause = function() {
if(that.audio == null){
log("audio null"); return;
}
that.audio.pause();
AudioStatus.isPlaying = false;
};
that.playPause = function() {
if (that.audio.paused) {
self._play();
}
else {
self._pause();
}
};
/* callback to set or update playback position */
that.updateProgress = function(value) {
if(that.audio == null){
log("audio null"); return;
}
that.audio.currentTime = value; // <<<--- it does NOT work if I call from XMLHttpRequest response but it DOES if it is called from a timer expired call back
};
that.isAudioPlaying = function(){
return AudioStatus.isPlaying;
};
};
This works for me.
if (!isNaN(audio.duration)) {
audio.currentTime = 0;
}
Hope it helps!
This solved it for me:
$("p#sound audio").on('loadedmetadata', () => {
$("p#sound audio").get(0).load();
});
So I could later set the currentTime without worrying about whether the audio was loaded or not.
I had a similar problem when trying to play a HLS stream through an HTML audio element.
No matter where and how I tried to set the currentTime property on the audio element, it wouldn't work until about 2.5s after calling audioElement.play().
//Calling this
htmlElement.currentTime = 5.5;
console.log('currentTime: '+htmlElement.currentTime);
//Would return 'currentTime: 0';
What did work tho, was if I called that in a timeout with 2500-3000ms delay.
After a day of debugging to pinpoint the element state in which it would allow me to set the currentTime property, so I wouldn't have to rely on a fixed timeout.
My solution was to listen to 3 HTMLMediaElement events (https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement#events): loadedmetadata, loadeddata and canplaythrough and setting custom flags when they were first reported.
When all 3 were reported and the HTMLMediaElement.duration property was bigger than 0, I set the HTMLMediaElement.currentTime to my desired value inside the first HTMLMediaElement timeupdate event callback.
This can be summed up in the following code snippet:
let audioElementStates = {
'metadataloaded': false,
'dataloaded': false,
'canplaythrough': false
};
let shouldSetOldTime = true;
let audioElement = document.getById('audio-element');
audioElement.src = 'https://somedomain.com/hlsplaylist.m3u8';
audioElement.addEventListener('loadedmetadata',()=>{audioElementStates.metadataloaded = true;});
audioElement.addEventListener('loadeddata',()=>{audioElementStates.dataloaded = true;});
audioElement.addEventListener('canplaythrough',()=>{audioElementStates.canplaythrough = true;});
audioElement.addEventListener('timeupdate',()=>{
if(shouldSetOldTime &&
audioElement.duration>0 &&
audioElementStates.metadataloaded &&
audioElementStates.dataloaded &&
audioElementStates.canplaythrough){
audioElement.currentTime = 5.5;
shouldSetOldTime=false;
}
});
audioElement.play();
For reference I was using this with Vue.js in a Quasar framework mobile app packaged with Apache Cordova.
NOTE: The loadedmetadata check can probably be skipped, since loadeddata would presumably never fire before loadedmetadata.