EDIT #1 with connexo’s solution
Within the following HTML, I want to click in the parent 'li' to play the child 'audio'
The problem is noted in the alert calls within the JS; i.e., trying to compare a HTMLLIElement with a HTMLAudioElement.
How can I successfully do that?
HTML:
I have several:
<li>
text here<br>
<audio controls preload="auto">
<source src="aSong.mp3">
</audio>
</li>
JS:
$('li:has(audio)').on("click", function(evt) {
var m, theSongInArray,
// thisSong = $(this)[0]; // HTMLLIElement
// now a HTMLAudioElement thanks to connexo
$thisLI = $(this);
thisSong = $thisLI.find('audio')[0];
// itsAllSongs defined elsewhere
for (m=0; m < itsAllSongs.length; m++)
{
// HTMLAudioElement
theSongInArray = itsAllSongs[m];
if (thisSong == theSongInArray)
{
if ( thisSong.paused )
thisSong.play();
else
{
thisSong.pause();
thisSong.currentTime = 0;
}
}
}
});
Note: connexo's solution is perfect - but now I have a new problem =
My intent above, which connexo has solved, was to be able to click anywhere within 'li:has(audio)' and have the toggling effect between .play() and .pause() .. and it now does that.
BUT, now the anywhere within 'li:has(audio)' seems to not include the play icon of:
If I click on the slider, the progress bar, the time-stamp, or anywhere else on the long <audio> control it works great, but not on the play icon on the far left of the above control.
SUPER wierd - this 2nd challenge goes away (I can then click just on the play icon) if I remove:
$('li:has(audio)').on("click", function(evt) {
})
Hopefully, connexo has another brilliant solution.
EDIT #2 = solution of the current problem:
Now, I can click on either the li area surrounding the <audio> element, or on just the <audio> element itself. As a matter of interest, when I click on just the play icon of <audio>, the song will play and, if I click on the position slider, the slider will move without playing/pausing the song itself:
/*
alert(evt.target); // = HTMLLIElement, or HTMLAudioElement
*/
if (evt.target.id.length == 0)
thisSong = $(this).find('audio')[0]; // HTMLLIElement -> HTMLAudioElement
else // >= 3, e.g., = "SSB" or "AIRFORCE"
thisSong = $(this)[0]; // [object HTMLAudioElement]
Related
I have created a flip tile memory game. A sequence of tiles will flip over displaying a colour. The user must remember the sequence and repeat it. When the user selects correctly a correct Mp 3 is played. On the Iphone if the tiles are selected quickly the audio isn't being played for each touch, its as though the audio is being skipped for some. link
const elements = {
gameContainer: $('#game-container'),
gameMenu: $('#game-menu'),
audioPlayer: document.querySelector('#player'),
audioPlayer2: document.querySelector('#player2'),
audioPlayer3: document.querySelector('#player3'),
tiles: $('.tile'),
correctAlert: $('#correct-alert'),
wrongAlert: $('#wrong-alert'),
failAlert: $('#fail-alert'),
alertModal: $('#alert-modal'),
stageNumber: $('.stage-number'),
maxStageNumber: $('.max-stage-number'),
gamemodeCheckbox: $('#gamemode-checkbox'),
stageProgress: $('#stage-progress'),
waitText: $('#wait-text'),
wonAlert: $('#won'),
goText: $('#go-text')
};
function tileClicked(tile) {
console.dir(tile)
// only allow clicking on tiles when game is started and game is not showing pattern
if (!game.showing && game.started && !tile.classList.contains('flip-card-onclick')) {
flipTile(tile);
// check if game reached maximum number of stages i.e. game has been won
if (game.playerMove <= game.maxStageNumber) {
// check if current move (tile clicked) matches the tile in the generated pattern
if (parseInt(tile.id) == game.currentGame[game.playerMove]) {
// increase the pattern pointer
game.playerMove++;
// play sound when correct tile has been clicked
elements.audioPlayer.pause();
elements.audioPlayer.currentTime = 0;
elements.audioPlayer.play();
// check if we reached the end of the current pattern
if (game.playerMove == game.currentGame.length) {
// update the progress bar
elements.stageProgress.css('width', `${(game.currentGame.length / game.maxStageNumber) * 100}%`);
// show alert prompting user to go to the next stage
elements.correctAlert.modal('show');
}
// current move did not match current pattern, wrong move
} else {
if (game.strictGamemode) {
elements.audioPlayer2.play();
// show fail alert and prompt to restart or exit game if strict mode has been selected
elements.failAlert.modal('show');
} else {
// show wrong move alert and prompt to show pattern again
elements.audioPlayer2.play();
elements.wrongAlert.modal('show');
}
}
}
}
}
<!--Audio Player-->
<audio controls id="player" class="d-none">
<source id="player-src" src="assets/audio/correct.mp3">
</audio>
<audio controls id="player2" class="d-none">
<source id="player-src-2" src="assets/audio/incorrect.mp3">
</audio>
<audio controls id="player3" class="d-none">
<source id ="player-src-3" src="assets/audio/won.mp3">
</audio>
It is very hard to tell where your bug comes from, so your solution may not be so easy to find. Some research might tell you some things but otherwise there is an alternative you could try.
The Web Audio API is an interface where you can get more control over the audio you play. So in your case instead of manipulating the <audio> element, use the Web Audio API to play your audio files.
Down here I've created a snippet which utilizes this API. It currently selects all of your <audio> elements and extracts the sound into a node which then can use to play the sound. This gives you control over how the sound plays.
So here it creates an object, which is stored in the sounds constant, that holds all the names as keys and the players as values. Something like this:
const sounds {
'correct': MediaElementAudioSourceNode,
'incorrect': MediaElementAudioSourceNode,
'won': MediaElementAudioSourceNode
};
Each of those MediaElementAudioSourceNode are the sounds which can be played. Later in the script there is a playSound function, which plays one of the sounds found in your sounds object.
const AudioContext = window.AudioContext || window.webkitAudioContext;
const audioContext = new AudioContext();
const audioElements = document.querySelectorAll('audio');
const createAudioSources = audioElements => {
const audioSources = {};
for (const audioElement of audioElements) {
const name = audioElement.dataset.name;
const track = audioContext.createMediaElementSource(audioElement);
audioSources[name] = track;
}
return audioSources;
};
const sounds = createAudioSources(audioElements);
function playSound(track) {
const sound = sounds[track];
if (sound === undefined) return;
sound.connect(audioContext.destination);
sound.start(audioContext.currentTime);
};
playSound('correct');
playSound('incorrect');
playSound('won');
So all of this could be added above your original script so that the sound files are loaded and ready for use. And then use the playSound() function anywhere in your script whenever you want to play anyone of the sounds. Example below:
function tileClicked(tile) {
console.dir(tile)
// only allow clicking on tiles when game is started and game is not showing pattern
if (!game.showing && game.started && !tile.classList.contains('flip-card-onclick')) {
flipTile(tile);
// check if game reached maximum number of stages i.e. game has been won
if (game.playerMove <= game.maxStageNumber) {
// check if current move (tile clicked) matches the tile in the generated pattern
if (parseInt(tile.id) == game.currentGame[game.playerMove]) {
// increase the pattern pointer
game.playerMove++;
// play sound when correct tile has been clicked
playSound('correct');
// check if we reached the end of the current pattern
if (game.playerMove == game.currentGame.length) {
// update the progress bar
elements.stageProgress.css('width', `${(game.currentGame.length / game.maxStageNumber) * 100}%`);
// show alert prompting user to go to the next stage
elements.correctAlert.modal('show');
}
// current move did not match current pattern, wrong move
} else {
if (game.strictGamemode) {
playSound('incorrect');
// show fail alert and prompt to restart or exit game if strict mode has been selected
elements.failAlert.modal('show');
} else {
// show wrong move alert and prompt to show pattern again
playSound('incorrect');
elements.wrongAlert.modal('show');
}
}
}
}
}
Also, add a data-name attribute to each <audio> element so that JavaScript knows how to call each player and it's accompanying sound.
<audio controls id="player" class="d-none" data-name="correct">
<source id="player-src" src="assets/audio/correct.mp3">
</audio>
<audio controls id="player2" class="d-none" data-name="incorrect">
<source id="player-src-2" src="assets/audio/incorrect.mp3">
</audio>
<audio controls id="player3" class="d-none" data-name="won">
<source id ="player-src-3" src="assets/audio/won.mp3">
</audio>
All my code above is untested and might throw an error, or worse, makes no difference at all. But hey, at least it's worth a shot.
I am using Flickity to create a slider of images and videos. When changing slides, I want to play the video on the current slide (which has a class of .is-selected), and then pause again once I move to the next slide.
The code below manages to find any videos within the slider, but plays them on any slide change, rather than specifically for the slide they are within. Any help would be much appreciated.
// Init Flickity
var $carousel = $('.carousel');
$carousel.flickity();
// On slide change
$carousel.on('change.flickity', function() {
// Find videos
var currentSlide = $(this).find('.is-selected');
var video = $(this).find('video');
// Play videos
video[0].play();
});
What you want to do is pause all videos on every slide change and only play the one on the selected slide. I did something similar (without jQuery):
var carousel = document.querySelector(".carousel");
var flkty = new Flickity(carousel, {
// ..
});
flkty.on("change", function() {
console.log("Flickity active slide: " + flkty.selectedIndex);
flkty.cells.forEach(function(cell, i) {
if (cell.element == flkty.selectedElement) {
// play video if active slide contains one
// use <video playsinline ..> in your html to assure it works on ios devices
var video = cell.element.querySelector("video");
if (video) {
console.log("playing video " + i);
video.play();
}
return;
}
// pause all other videos
var video = cell.element.querySelector("video");
if (video) {
console.log("pausing video " + i);
video.pause();
}
});
});
As I stated in the comment, you might wanna use the "playsinline" attribute for your videos, otherwise you'll run into problems on iOS.
Also: using the "settle" event instead of "change" allows you to start playing once the active slide settled at its end position.
after many hours looking for a solution by myself, I'm asking for your help.
I'd like to play audio by clicking on an img and this is how I organised my code in HTML, audio will be different for each class : a_ru.mp3, b_ru.mp3 etc.
<span class="btn-audio-lexique">
<audio src="http://localhost/linguami-offline/fr/mp3/a_ru.mp3"></audio>
<img src="http://localhost/linguami-offline/img/circled-play.png">
</span>
<span class="btn-audio-lexique">
<audio src="http://localhost/linguami-offline/fr/mp3/b_ru.mp3"></audio>
<img src="http://localhost/linguami-offline/img/circled-play.png">
</span>
JS part which is unfortunately not workings looks like that
var classname = document.getElementsByClassName("btn-audio-lexique");
function playSound() {
var audio = classname.firstElementChild;
audio.play();
};
Array.from(classname).forEach(function(element) {
element.addEventListener('click', playSound);
});
I also tried with a for loop
for (var i = 0; i < classname.length; i++) {
classname[i].addEventListener('click', playSound);
}
console returns me a "audio is undefined error".
I would like to avoid creating a different id for all the audio in my page.
does anyone have an idea how should I organise my code for this works?
Thanks in advance !
You have few mistakes in your code:
I. Do not add event listener to each action element. It's bad for performance reasons. You should add event listener to some common parent element, which holds all the children buttons.
document.querySelector('.myAwesomeContainer').addEventListener('click', function(e) {
var target = e.target;
if (target && target.classList.contains('btn-audio-lexique')) {
var audio = target.firstElementChild;
// var audio = target.querySelector('audio');
if (audio) {
audio.play();
}
}
});
II. You don't necessarily need to mess up with audio elements. You can use AudioAPI and play sounds like:
var audio = new Audio('audio_file.mp3');
audio.play();
III. DO NOT use <span>, <div> or any HTML tag as clickable stuff except <button>, <a> Because you're destroing semantic markup and reducing accessability of you're application to the ground.
The actual reason of error in your current code, is hidden in that row:
var audio = classname.firstElementChild;
You're trying to get audio element not from the actual SPAN which user clicked, but from the array-like object of all your spans. Because classname is a LIST of all spans. What can you do in that situation? Just add event param to your playSound function. Because addEventListener will pass that param when event will be triggered:
var classname = document.getElementsByClassName("btn-audio-lexique");
function playSound(e) {
// Get actual clicked element
var target = e.target;
var audio = target.firstElementChild;
if (audio) {
audio.play();
}
};
Array.from(classname).forEach(function(element) {
element.addEventListener('click', playSound);
});
I have designed an i-phone-like screen on a web browser where I am testing this application I am in the process of building. It works great up until the point where I want to call out another set of videos.
What works
The application is structured so that when the user sees the screen she is directed to a channel that has a vertical video.
The buttons on the top left and top right advance to the next and the previous video.
<div id="topVid" class="videoContainer">
<div class="topHorizontalButtonRow">
</div>
<video class="topVid" loop onclick="this.paused? this.play() : this.pause()" >
<source src="videos/ParisisBurning_660x370_I_went_to_a_ball.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
</div>
There is a "channel" button that shows the user a smaller window if pressed, where the user can view other channels by clicking on a second set of buttons next and previous buttons.
<div id="bottomVid" class="videoContainerTwo hiddenElement">
<div class="topHorizontalButtonRow">
<div class="buttonLeftTriangleBlue"></div>
<div class="buttonRightTriangleBlue"></div>
</div>
<video loop onclick="this.paused? this.play() : this.pause()" >
<source src="videos/Politics_Refugee_Sign.mp4" type="video/mp4">
Your browser does not support the video tag.
</video>
jquery show/hide smaller window:
$(".buttonTeardropChannelBlue").click( function (){
if( $("#bottomVid").is(':visible') ) {
$("#bottomVid").hide();
} else {
$("#bottomVid").show();
}
});
If the user wants to watch this specific channel, she can click on the smaller window, which hides the current window and advances to the other channel. The video can be clicked on, and once that happens, the user will be directed to the next channel.
Below is the code that works perfectly to advance the video of the current selection, and it contains the videos in arranged in an array.
var Vids = (function() {
var _currentId = -1;
var _urls =
["videos/ParisisBurning_370x660_Get_Into_The_Suits_Vert.mp4","videos/ParisisBurning_370x660_School_Vert.mp4","videos/ParisisBurning_660x370_I_came_I_saw_Vert.mp4", "videos/ParisisBurning_660x370_I_went_to_a_ball.mp4"]; // literal array
return {
next: function() {
if (++_currentId >= _urls.length)
_currentId = 0;
return this.play(_currentId);
},
prev: function() {
if (--_currentId < 0)
_currentId = _urls.length - 1;
return this.play(_currentId);
},
play: function(id) {
var myVideo = document.getElementsByTagName('video')[0];
myVideo.src = _urls[id];
myVideo.load();
myVideo.play();
return false;
}
}
})();
What does not work
The issue: showing and hiding multiple video lists
However, the problem starts when I want to select a different class of videos, which has the exact same code except for different videos. I have changed the name of the function to say, VidsTwo but the problem remains.
var VidsTwo = (function() {
var _currentId = -1;
var _urls = ["videos/Politics_Atl_We_are_the_people.mp4","videos/Politics_Atlanta_Whose_Streets.mp4", "videos/Politics_Womens_March_Washington_CBS_VERT.mp4",
"videos/Politics_No_bans_no_walls_America_is_home_to_all_VERT.mp4",
"videos/Politics_Let_them_in_VERT.mp4",
"videos/Politics_Tear it Down_JFK_VERT.mp4",
"videos/Politics_This_is_What_America_Looks_Like_embrace.mp4",
"videos/Politics_This_land_was_made_VERT.mp4", "videos/Politics_We_need_an_independent_investigation_town_hall.mp4",
"videos/Politics_Just say no_town_hall_VERT.mp4", ]; // literal array
return {
next: function() {
if (++_currentId >= _urls.length)
_currentId = 0;
return this.play(_currentId);
},
prev: function() {
if (--_currentId < 0)
_currentId = _urls.length - 1;
return this.play(_currentId);
},
play: function(id) {
var myVideo = document.getElementsByTagName('video')[0];
myVideo.src = _urls[id];
myVideo.load();
myVideo.play();
return false;
}
}
})();
The issue remains: the buttons will continue to play the videos of the current channel in addition to the ones of the new channel, and it will not hide the current video. I understand it happens because in the javascript code, it uses the select element by tag which is "video". and all the array lists have "video" so it is playing all of them.
What is the best solution to this problem, given that I want to be able to separate the videos into categories "channels" that will have similar thematic content, and that this categories will be called by users as they look at a second smaller window of videos?
Core questions
Is there a way to have it NOT play a selection of arrays? What can I change in the Javascript code that will indicate that these separate video arrays do not belong to the same class? How can I make it clear in the code that these videos, although they are all videos, belong to different categories and therefore can only be played if their specific category is called?
Brainstorming solutions:
I am thinking I would probably need a second div that will have a
second row of buttons that call out the second function, since the
prev and next indicate a separate variable that was declared for each
class of videos...but this is getting a bit complicated for my newbie
skills:)
Or perhaps each video on a parent class should be saved on the
html itself as a hidden div and should be called by using "show
next child of parent div", as opposed to being saved as an array on
the javascript code?
The next step is adding marquee text to each video so maybe having
separate hidden divs on the html itself is a better solution than
having the videos stored as javascript arrays?
This is basically a prototype/beta for something that will become an
app so there is no database yet, (which will make it easier to
store this info eventually once I begin more in-depth user tests).
This complication is for testing only:)
UPDATE: I am still curious as to what the best solution would be, however I have decided, in this case, to add divs directly to the html and use jquery's next sibling selectors. Because I will have some text specific to some videos, they won't be properly connected to the javascript arrays anyway. I find the javascript array solution "cooler" but it is perhaps not the best in the end.
make Vids like this:
var Vids = function(vidArray=[]) {
var _currentId = -1;
var _urls = vidArray;
return {
next: function() {
if (++_currentId >= _urls.length)
_currentId = 0;
return this.play(_currentId);
},
prev: function() {
if (--_currentId < 0)
_currentId = _urls.length - 1;
return this.play(_currentId);
},
play: function(id) {
var myVideo = document.getElementsByTagName('video')[0];
myVideo.src = _urls[id];
myVideo.load();
myVideo.play();
return false;
}
}
};
then prepare your url array and call Vids:
var urls =["videos/ParisisBurning_370x660_Get_Into_The_Suits_Vert.mp4","videos/ParisisBurning_370x660_School_Vert.mp4","videos/ParisisBurning_660x370_I_came_I_saw_Vert.mp4", "videos/ParisisBurning_660x370_I_went_to_a_ball.mp4"];
Vids(urlf).play(3); //Replace 3 with any id
I have a little html5 application where you can play a sound by clicking a button.
I have a function that adds an <audio> tag to a <div> with an id "playing." The sound removes itself when it is done.
function sound(track){
$("#playing").append("<audio src=\"" + track + "\" autoplay onended=\"$(this).remove()\"></audio>");
}
For the button I have:
<button onclick="sound('sounds/tada.mp3')">Tada</button>
When I click the button, an <audio> briefly appears in the element inspector and disappears when it is finished, just the way I want it, but after triggering it two times, it just stops working in Chrome, at least. There are no errors in the console either.
What is going on?
Get rid of the onclick/onend in your HTML and reference the button in your js:
HTML
<button id='tada' sound_url='sounds/tada.mp3'>Tada</button>
And the JS
var sound = function(track){
$("#playing").append("<audio id='played_audio' src='\" + track + \"' autoplay='true'></audio>");
}
$('#tada').on('click', function () {
var sound_url = $(this).attr('sound_url');
sound(sound_url);
});
$('#playing').on('end', 'played_audio', function() {
$(this).remove();
});
Okay, lets see..
var audioURL = "http://soundbible.com/mp3/Canadian Geese-SoundBible.com-56609871.mp3";
var audioEl = null;
function removeAudio() {
if (audioEl && audioEl.parentNode)
audioEl.parentNode.removeChild(audioEl);
}
function sound() {
removeAudio();
audioEl = document.createElement("audio");
audioEl.src = audioURL;
audioEl.controls = true;
audioEl.addEventListener("ended", removeAudio); // <-- Note it's ended, not end!
document.getElementById("playing").appendChild(audioEl);
audioEl.play();
}
document.getElementById("tada").addEventListener("click", sound);
<div id="playing">
</div>
<button id="tada">Tada</button>
I'm not seeing any problems with this script.
Decide audioURL, set audioEl to null as it will be used later
When the element with ID "tada" is clicked, run our sound function.
Remove the audio.
Create the audio element.
When the audio is finished, remove the audio.
Append the audio to the element with ID "playing".
Play the audio.
One thing to note is that I use the ended event, not the end event.
(This answer is here because Andrew really wants us to answer it.)