I'm looking for a way to make html5 audio play as soon as button is clicked. Right now, it does work; but not very reliably.
My file is a 300Kb MP3 (128Kbps) that is hosted on AWS Cloudfront.
<audio id="i-audio" src="https://cdn/url/to/i-audio.mp3" preload="auto" type="audio/mp3"></audio>
this.iAudio = document.getElementById('i-audio');
$("#button").on('click', function() {
this.iAuido.play();
});
I learn that play() will return a promise; and I'm not sure how can I be sure that audio plays all the time; reliably.
Right now, the play would work at random times or just throw an error.
If you're getting error messages about Promises and .play(), you'll need to treat .play() as a Promise, see this article.
I didn't see any button#button so I'm assuming that's just a typo.
this has no context so it'll be referencing window (very useless)
this.iAudio = document.getElementById('i-audio');
A variable (like var iAudio = ... or const iAudio = ...) should've been declared.
this has context of the element listening to the click event
$("#button").on('click', function() {
this.iAuido.play();
});
Can't imagine that this code worked even randomly it just gets worse as I read the next line. Let's assume iAudio is correct and references the <audio> tag, but for some reason this is prefixed to it (wHy?!?). this would reference the button so every segment of a line of code is all sorts of wrong.
const playback = (e) => {
const mp3 = $('.mp3')[0];
let action = mp3.paused || mp3.ended ? mp3.play() : mp3.pause();
return action;
}
playback().then(() => console.log('Promise Resolved')).catch(error => $('.btn').on('click', playback));
button {
font-size: 5rem;
color: cyan;
border: 0;
background: none;
cursor: pointer
}
button:focus {
outline: none
}
button:hover {
color: tomato;
}
<audio class="mp3" src="https://glsbx.s3.amazonaws.com/-/dd.mp3" preload="auto"></audio>
<button class='btn' type='button'>▶</button>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
Related
I've been building an image uploader that is now theoretically complete, but it is not working in Safari.
I initially thought the problem was because DataTransfer, or more specifically dataTransfer.files was not supported, but this is now supported in Desktop Safari (Version 14.1 +). Although there is no support at all in iOS I will be having the file-picker only (no drag and drop) on iOS so this isn't an issue.
The Problem / Scenario on Desktop Safari
When a user clicks the 'select files' link and submits the images, it all works OK.
When a user drags and drops images the file preview images are duplicated (e.g. 2 images shows 4 previews). When submitted, only the 2 images are uploaded though (which is correct obviously).
When a user clicks the 'select files' link instead of using the drag and drop functionality and then deletes an image from the image previews, the image previews are then duplicated (similar to the way the are in point 2. above).
Logically this would make me think the problem is with the change event listener, but I can't seem to fix it.
Any help would be hugely appreciated.
const dropZone = document.getElementById("drop-zone"),
showSelectedImages = document.getElementById("show-selected-images"),
fileUploader = document.getElementById("standard-upload-files");
dropZone.addEventListener("click", (evt) => {
// assigns the dropzone to the hidden input element so when you click 'select files' it brings up a file picker window
fileUploader.click();
});
// Prevent browser default when draging over
dropZone.addEventListener("dragover", (evt) => {
evt.preventDefault();
});
fileUploader.addEventListener("change", (evt) => {
// this function is further down but declared here and shows a thumbnail of the image
[...fileUploader.files].forEach(updateThumbnail);
});
function getFileListItems(files) {
var transferObject = new ClipboardEvent("").clipboardData || new DataTransfer()
for (var i = 0; i < files.length; i++) transferObject.items.add(files[i])
return transferObject.files;
}
dropZone.addEventListener("drop", (evt) => {
evt.preventDefault();
// assign dropped files to the hidden input element
if (evt.dataTransfer.files.length) {
fileUploader.files = getFileListItems([...fileUploader.files, ...evt.dataTransfer.files]);
}
// function is declared here but written further down
[...evt.dataTransfer.files].forEach(updateThumbnail);
});
// updateThumbnail function that needs to be able to handle multiple files
function updateThumbnail(file) {
if (file.type.startsWith("image/")) {
let uploadImageWrapper = document.createElement("article"),
removeImage = document.createElement("div"),
thumbnailElement = new Image();
// 'x' that deletes the image
removeImage.classList.add("remove-image");
removeImage.innerHTML =
'<svg id="remove-x" viewBox="0 0 150 150"><path fill="#000" d="M147.23,133.89a9.43,9.43,0,1,1-13.33,13.34L75,88.34,16.1,147.23A9.43,9.43,0,1,1,2.76,133.89L61.66,75,2.76,16.09A9.43,9.43,0,0,1,16.1,2.77L75,61.66,133.9,2.77a9.42,9.42,0,1,1,13.33,13.32L88.33,75Z"/></svg>';
// image thumbnail
thumbnailElement.classList.add("drop-zone__thumb");
thumbnailElement.src = URL.createObjectURL(file);
// appending elements
showSelectedImages.append(uploadImageWrapper); // <article> element
uploadImageWrapper.append(removeImage); // 'x' to delete
uploadImageWrapper.append(thumbnailElement); // image thumbnail
// Delete images
removeImage.addEventListener("click", (evt) => {
if (evt.target) {
var deleteImage = removeImage.parentElement;
deleteImage.remove();
fileUploader.files = getFileListItems([...fileUploader.files].filter(f => file !== f));
}
});
}
} // end of 'updateThumbnail' function
body {
margin: 0;
display: flex;
justify-content: center;
width: 100%;
}
form {
width: 30%;
}
#drop-zone {
border: 1px dashed;
width: 100%;
padding: 1rem;
margin-bottom: 1rem;
}
.select-files {
text-decoration: underline;
cursor: pointer;
}
/* images that are previewed prior to form submission*/
.drop-zone__thumb {
width: 200px;
height: auto;
display: block;
}
#remove-x {
width: 1rem;
height: 1rem;
}
#submit-images {
margin: 1rem 0;
}
#show-selected-images {
display: flex;
}
<form id="upload-images-form" enctype="multipart/form-data" method="post">
<h1>Upload Your Images</h1>
<div id="drop-zone" class="drop-zone flex">
<p class="td text-center">DRAG AND DROP IMAGES HERE</p>
<p class="td text-center" style="margin: 0">Or</p>
<p class="tl text-center select-files text-bold pointer">Select Files</p>
</div>
<input id="standard-upload-files" style="display:none" style="min-width: 100%" type="file" name="standard-upload-files[]" multiple>
<input type="submit" name="submit-images" id="submit-images" value="SUBMIT IMAGES">
<div id="show-selected-images"></div>
</form>
The problem is that Safari fires a new (trusted o.O) event when you set the .files property of your <input>. (BUG 180465)
Given they do fire that event synchronously, you could workaround that by using a simple flag:
let ignoreEvent = false; // our flag
const inp = document.querySelector("input");
inp.onchange = (evt) => {
if (ignoreEvent) {
console.log("this event should be ignored");
}
else {
console.log("A true change event");
}
};
const dT = new DataTransfer();
dT.items.add(new File(['foo'], 'programmatically_created.txt'));
// right before setting the .files
ignoreEvent = true;
inp.files = dT.files;
// the change event fired synchronously,
// lower the flag down
ignoreEvent = false;
<input type="file" id="inp">
Which in Safari will output "This event should be ignored".
Now, I can't help but remind that setting the FileList of an <input> like that is still a hack. You really shouldn't use this in production. (I did amend my answer to make it clearer.)
So, please go with a simple Array and a FormData for the upload. Moreover since you don't even show the original file picker <input>.
You apparently already did copy-paste a previous answer that you did not understand, so to avoid you falling in this (comprehensible) trap again, I'll just highlight the key points you will need for your implementation, and I won't give you a working example, on purpose.
So first you need to define an Array ([]) which will store the files your user did select and which is accessible to all of the following functions.
In both the drop and input's change events, you will update this Array with the Files newly selected (you can either append the new Files, or replace them, it's your call).
Then you will add a new event listener to the <form> element's submit event. This event will fire when the submit button is clicked.
Its default action would be to POST the <form>'s content to your server, but since we want to include our own Array and not the <input>'s file list (since it could actually be empty), we don't want this default action to occur, so we will call event.preventDefault() to avoid that.
Now, we still want to send something to the server, so we will build a new form, from scratch.
That's where we'll create a new FormData object, and use its append() method to store our files. The first argument of this method should be the field name, which is also the <input>'s name attribute value. The second one should be a single file, so we'll need to call this method as many times as we have items in our Array. Since we deal with File objects, we don't need the third argument.
Once our FormData is complete, we just have to upload it to the server.
To do so you, in modern browsers you can simply use the fetch() method, passing in the URL to the server, and an option bag looking roughly like
{
method: "POST",
body: formData // the FormData object we just created
}
If you need to support older browsers, you can also do the same with an XMLHttpRequest object, passing the FormData in its .send(formData) method.
Since OP apparently can't get out of this despite the step by step explanations, here is a jsfiddle.
i am making a custom video player in which there is an overlay containing the controls of the video player
my player starts to work in full length and height.
now i want to hide the overlay after 5 seconds i stop the mouse over.
now the problem is that when the below function mouse over in .ts file is called the synchronization of the timer is harmed.
so if i move my mouse continuously the overlay starts to flicker.
please provide me the solution to the problem.
following is my html code
<div class="video-container" #videoFullscreen>
<div class="video-wrapper" mouse-move>
<video class="video video-js" data-dashjs-player id="myVideo" autoplay #videoPlayer>
<source src="{{ videoSource }}" type="video/mp4" />
</video>
<!-- overlay -->
<div class="overlay" [class.hideOverlay]="hideTop">
<!-- top controls -->
.
.
<!-- lower controls -->
</div>
</div>
this is my type script code
#HostListener('document:mousemove', [ '$event' ]) //fuction to display and hide element sue to mouseover
onMouseMove($event) {
this.hideTop = false;
setTimeout(() => {
this.hideTop = true;
}, 5000);
}
this is my css code :
.overlay {
display: flex;
}
.hideOverlay {
display:none;
}
please help me to solve this problem.
Store the lastHover time and compare against it.
private lastHover = 0;
#HostListener(...)
onMouseMove($event) {
this.lastHover = new Date().getTime()
This.hideTop = true;
setTimeout( () => {
...
if(lastHover + 5000 < new Date().getTime()) {
This.hideTop = true;
}
}, 5000)
}
A neat solution would be to use rxjs to solve this like shown below:
ngOnInit(): void {
fromEvent<MouseEvent>(document, 'mousemove').pipe(tap(() => {
console.log("show it!");
this.hideTop = false
}), switchMap((event) =>
timer(5000).pipe(tap(() => {
console.log("hideit");
this.hideTop = true;
}))
)).subscribe();
}
Don't forget to unsubscribe if your component gets destroyed to prevent memory leaks!
First we make an Observable from the documents mousemove event.
Now if the event triggers we set hideTop to true.
And here comes the interesting part: we use switchMap with a timer Observable. switchMap automatically unsubscribes from the inner Observable if the outer one emits a new value. Therefore the inner Observable only emits after the user actually stopped moving the mouse for 5 seconds.
Apologies that my answer is in jQuery, but the concept is fairly basic
What we need to do is check if the timeout event has already been fired, and reset it on a mousemove event during that time. This is done by checking if the class for hiding the element is applied or not
//Timer variable
var timer;
//Detect mousemove event on parent element
$("html").on("mousemove", "#outer", function() {
//Is the element already hidden?
if ($("#inner").hasClass("hide")) {
//Show the element
$("#inner").removeClass("hide")
} else {
//Reset the timer to 5 seconds
clearTimeout(timer);
timer = setTimeout(hideBox, 5000);
}
})
function hideBox() {
$("#inner").addClass("hide")
}
https://jsfiddle.net/xcL52zf3/1/
You'll need to swap out the jQuery event handlers and element targetting with the equivalent for you TypeScript library
Is it possible to force VideoJS to stop streaming videos that no longer have the main focus on a page or that have been paused? If, for example, I load two videos and then decide to make one of them fullscreen so the other loses focus or if I switch tab and both lose focus I'd like to not only pause the video from playing but prevent the player continuing to download ( via xhr ) the video stream.
It is simple to call the play and pause methods when needed but I have yet to figure a way that this streaming might also be paused - can anyone suggest how this might be done. I have toyed with removing the video src attribute having seen mention of that somewhere but that results in several errors and did not resolve the problem.
Initially I was using the HLS code ( videojs-contrib-hls.js ) but read somewhere that it might be better using the http streaming code ( videojs-http-streaming.js ) ~ same results.
When playing with the videojs streaming test page the default playlist doesn't exhibit this behaviour but other playlists do cause this downloading behaviour.
The code below is a pseudo facsimile of the real code but works as it stands to reproduce the errors. When either video is maximised &/or both videos are paused you can observe network requests in the console.
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='utf-8' />
<title>Stop VideoJS buffering other videos that do not have focus</title>
<link href='https://unpkg.com/video.js/dist/video-js.css' rel='stylesheet' />
<style>
body,body *{box-sizing:border-box;}
div.container{ width:600px; height:400px; margin:1rem; display:inline-block;clear:none; padding:0; }
video{max-width:600px;max-height:400px;opacity:1}
[type='button'].play{ width:100%; padding:1rem; margin:auto; float:none; position:relative; top:130px; }
[type='button'][name='ctrl']{position:absolute;top:0;left:0}
</style>
<script src='//cdnjs.cloudflare.com/ajax/libs/video.js/7.7.1/video.min.js'></script>
<script src='//unpkg.com/#videojs/http-streaming/dist/videojs-http-streaming.js'></script>
<script>
document.addEventListener('DOMContentLoaded',()=>{
let urls=[
'https://edge1.dashmedia.tv/onestudio/gonefishing/playlist.m3u8?fluxustv.m3u8',
'https://d2zihajmogu5jn.cloudfront.net/bipbop-advanced/bipbop_16x9_variant.m3u8'
];
let players={};
let videos={};
const maximise=function(e){
let player=players[ this.previousElementSibling.id ];
player.requestFullscreen();
};
/*
For any video currently playing that is NOT fullscreen
or not visible ( calculated by IntersectionObserver not shown here )
should be paused and the network stream should be stopped
for the duration. How?
The methods below cause errors. Simply using `player.pause()` will
happily pause the video but it will continue to stream the file
and so causing excessive network usage for hidden items.
*/
document.addEventListener( 'fullscreenchange', event=>{
let player;
let video;
const isFullscreen=function(){
return document.fullscreenElement!==null;
};
Object.keys( videos ).map( id =>{
player=players[ id ];
video=videos[ id ];
if( video && video != event.target.querySelector('video') ){
if( isFullscreen() )player.pause();
else player.play();
}
console.info('%cPlayer:%s Paused:%s','color:yellow;background:fuchsia;',id,player.paused());
});
});
/* event listener to play/stop videos */
document.querySelector('input[name="ctrl"]').addEventListener('click', function(e){
urls.forEach( ( url, index )=>{
let id='player_'+index;
let oCont=document.createElement('div');
oCont.className='container';
let oVideo=document.createElement('video');
oVideo.id=id;
oVideo.width=600;
oVideo.width=400;
let oBttn=document.createElement('input');
oBttn.className='play';
oBttn.type='button';
oBttn.value='Maximise';
oBttn.onclick=maximise;
oCont.appendChild( oVideo );
oCont.appendChild( oBttn );
document.body.appendChild( oCont );
let video_options={
'controls':true,
'autoplay':true,
'preload':'auto',
'muted':true,
children:[]
};
let video=window.player = videojs( id, video_options );
video.src({ src:url, type:'application/x-mpegURL' } );
video.ready( ()=>{
video.name=id;
video.errorDisplay=false;
video.play();
});
video.on('error',function(e){
e.stopImmediatePropagation();
console.info( '%cError:%s\nCode:%s', 'background:lime;color:black;', this.error().message, this.error().code )
video.dispose();
})
players[ id ]=video;
videos[ id ]=oVideo;
});//end forEach
});//end play click handler
});//end listener
</script>
</head>
<body>
<input type='button' name='ctrl' value='Play videos' />
</body>
</html>
I have a custom video player with JS, html and css. Crux of my issue here is I didn't anticipate scaling this from one video, to two videos and I'm looking to refactor this so I can play multiple videos on one page. I've tried rewriting everything into a forEach and haven't been able to crack it. Really just need someone to nudge me in the right direction here:
Fiddle
My thinking was to simply change const player = document.querySelector('.custom-video-player'); to const players = document.querySelectorAll('.custom-video-player'); and then scope something like:
players.forEach((player) => {
// declare all the consts here... and event listeners
})
However, this approach isn't really working. Ideally I wanted to be lazy and not rewrite each instance of player. At this point I'm pretty stuck...
HTML
<div class="cs__video">
<div class="custom-video-player">
<video class="player__video viewer" src="http://techslides.com/demos/sample-videos/small.mp4"></video>
</div>
<div class="custom-video-player">
<video class="player__video viewer" src="http://techslides.com/demos/sample-videos/small.mp4"></video>
</div>
</div>
JS
/* custom video player javascripts */
// declaring elements
const player = document.querySelector('.custom-video-player');
const video = player.querySelector('.viewer');
/* Build out functions */
function togglePlay() {
console.log('playing');
const method = video.paused ? 'play' : 'pause';
video[method]();
}
/* event listeners */
video.addEventListener('click', togglePlay);
video.addEventListener('play', updateButton);
video.addEventListener('pause', updateButton);
toggle.addEventListener('click', togglePlay);
You may find it easier to manage the multiple players if you create each one from a class that includes all the relevant setup and methods.
Once you create the class for all players it's easy to create as many as you like.
Here's an example that creates an array of two players from an array of video sources (also available as a fiddle).
class Player {
// We call `new Player()` with two arguments, the id
// and the video source
constructor(id, src) {
// We assign both id and src to the class
this.id = id;
this.src = src;
// Then we call two functions, one to generate the
// video HTML, and one to add it to the page
const html = this.generateHTML(id);
this.addHTMLToDOM(html);
}
// We use a template literal to build our HTML
// using the id and src we passed into the class earlier
generateHTML() {
return (
`<div data-player=${this.id}>Player ${this.id}</div>
<video controls width="250">
<source src="${this.src}" type="video/mp4" />
Sorry, your browser doesn't support embedded videos.
</video>`
);
}
// This method simply adds the player HTML
// to the document body
addHTMLToDOM(html) {
document.body.insertAdjacentHTML('beforeend', html);
}
// play and pause are a couple of example methods for
// player control. `return this` allows for the methods
// to be chained (see below)
play() {
console.log(`Playing video ${this.id}`);
return this;
}
pause() {
console.log(`Pausing video ${this.id}`);
return this;
}
}
// An array of video sources
const srcs = [
'http://techslides.com/demos/sample-videos/small.mp4',
'http://techslides.com/demos/sample-videos/small.mp4'
]
// `map` over the srcs array to create an array of new players
const players = srcs.map((src, i) => new Player(++i, src));
// An example to show how we can call the player instance methods
players[0].play().pause();
players[1].play().pause();
I've made a mistake. I paired my functionality to .on('click', ...) events. My system installs certain items and each item is categorized. Currently, my categories are [post, image, widgets], each having its own process and they are represented on the front-end as a list. Here's how it looks:
Each one of these, as I said, is paired to a click event. When the user clicks Install a nice loader appears, the <li> itself has stylish changes and so on.
I also happen to have a button which should allow the user to install all the items:
That's neat. Except...there is absolutely no way to do this without emulating user clicks. That's fine, but then, how can I wait for each item to complete (or not) before proceeding with the next?
How can I signal to the outside world that the install process is done?
It feels that if I use new CustomEvent, this will start to become hard to understand.
Here's some code of what I'm trying to achieve:
const installComponent = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
return resolve();
}, 1500);
});
};
$('.item').on('click', (event) => {
installComponent().then(() => {
console.log('Done with item!');
});
});
$('#install-all').on('click', (event) => {
const items = $('.item');
items.each((index, element) => {
element.click();
});
});
ul,
ol {
list-style: none;
padding: 0;
margin: 0;
}
.items {
display: flex;
flex-direction: column;
width: 360px;
}
.item {
display: flex;
justify-content: space-between;
width: 100%;
padding: 12px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
margin: 0;
}
.item h3 {
width: 80%;
}
.install-component {
width: 20%;
}
#install-all {
width: 360px;
height: 48px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<ul class="items">
<li class="item" data-component-name="widgets">
<h3>Widgets</h3>
<button class="install-component">Install </button>
</li>
<li class="item" data-component-name="post">
<h3>Posts</h3>
<button class="install-component">Install </button>
</li>
<li class="item" data-component-name="images">
<h3>Images</h3>
<button class="install-component">Install </button>
</li>
</ul>
<button id="install-all">Install All</button>
As you can see, all clicks are launched at the same time. There's no way to wait for whatever a click triggered to finish.
This is simple architectural problems with your application that can be solved by looking into a pattern that falls into MVC, Flux, etc.
I recommend flux a lot because it’s easy to understand and you can solve your issues by separating out your events and UI via a store and Actions.
In this case you would fire an action when clicking any of these buttons. The action could immediately update your store to set the UI into a loading state that disables clicking anything else and show the loader. The action would then process the loader which can be monitored with promises and upon completion the action would finalize by setting the loading state in the store to false and the UI can resolve to being normal again. The cool thing about the proper separation is the actions would be simple JS methods you can invoke to cause all elements to install if you so desire. Essentially, decoupling things now will make your life easier for all things.
This can sound very complicated and verbose for something as simple as click load wait finish but that’s what react, angular, flux, redux, mobx, etc are all trying to solve for you.
In this case I highly recommend examining React and Mobx with modern ECMaScript async/await to quickly make this issue and future design decisions much easier.
What you should do is to declare a variable which will store the installation if it's in progress. And it will be checked when you are trying to install before one installation is complete.
var inProgress = false;
const installComponent = () => {
inProgress = true;
return new Promise((resolve, reject) => {
if(inProgress) return;
else{
setTimeout(() => {
inProgress = false;
return resolve();
}, 1500);
}
});
};
I'd be looking to implement something like this:
let $items = $('.items .item');
let promises = new Array($items.length);
// trigger installation of the i'th component, remembering the state of that
function startInstallOnce(i) {
if (!promises[i]) {
let component = $items.get(i).data('component-name');
promises[i] = installComponent(component);
}
return promises[i];
}
// used when a single item is clicked
$items.on('click', function(ev) {
let i = $(this).index();
startInstallOnce(i);
});
// install all (remaining) components in turn
$('#install-all').on('click', function(ev) {
(function loop(i) { // async pseudo-recursive loop
if (i === components.length) return; // all done
startInstallOnce(i).then(() => loop(i + 1));
})(0);
});