setSinkId only works with <audio> and not new Audio() - javascript

Trying to setSinkId on an audio node. I have noticed setSinkId only works in very specific circumstances and I need clarification. Examples behave similar in latest firefox and chrome.
This works:
index.html
<audio id="audio"></audio>
app.js
this.audio = document.getElementById('audio');
this.audio.src =... and .play()
this.audio.setSinkId(this.deviceId);
This is not OK beyond testing as now every player will be sharing a node. They each need a unique one.
This does not:
app.js
this.audio = new Audio();
this.audio.src =... and .play()
this.audio.setSinkId(this.deviceId)
This also doesn't work
app.js
this.audio = document.createElement('audio');
document.body.appendChild(this.audio);
this.audio.src =... and .play()
this.audio.setSinkId(this.deviceId)
Are there differences between new Audio, createElement, and audio present in HTML?
Why doesn't setSinkId work on a new Audio()?

For me it works both in Chrome and in Firefox (after I changed the prefs), for as long as I do request the permissions before-hand, and intialize the audio through an user-gesture.
For requesting access to the audio-output will become as easy as calling navigator.mediaDevices.selectAudioOutput() and let the user choose what they want, but until this is implemented, we still have to hack-around by requesting a media input instead, as explained in this answer.
So this script should do it:
btn.onclick = async (evt) => {
// request device access the bad way,
// until we get a proper mediaDevices.selectAudioOutput
(await navigator.mediaDevices.getUserMedia({audio:true}))
.getTracks().forEach( (track) => track.stop());
const devices = await navigator.mediaDevices.enumerateDevices();
const audio_outputs = devices.filter( (device) => device.kind === "audiooutput" );
const sel = document.createElement("select");
audio_outputs.forEach( (device, i) => sel.append(new Option( device.label || `device ${i}`, device.deviceId )) );
document.body.append(sel);
btn.textContent = "play audio in selected output";
btn.onclick = (evt) => {
const aud = new Audio(AUDIO_SRC);
aud.setSinkId(sel.value);
aud.play();
};
}
But since StackSnippets don't allow gUM calls, we have to outsource it, so here is a live fiddle.

It looks like when you create a new audio it takes some time to play media and be ready to use setSinkId. My solution (I don't like it too much, but is what I have right now) is to use a setTimeout function with one second or less. Something like this:
Replace
this.audio.setSinkId(this.deviceId)
By
setTimeout(() => {
this.audio.setSinkId(this.deviceId)
}, 1000);

Related

Jest testing HTML Media Element

I have written a web-app which plays selected mp3 files in order of selection. When it comes to testing I cannot get my jest tests to enter the onended() handler of the HTMLAudioElement.
I have tried to spyOn the play() method on the elements but cannot find a way to set the audio's ended attribute to true.
The code for playing the audio is as follows:
playAudioFiles = () =\> {
const audioFiles = this.getAudioFiles()
const audio = audioFiles[0]
let index = 1
audio.play()
audio.onended = () => {
if (index < audioFiles.length) {
audio.src = audioFiles[index].src
audio.play()
index++
}
}
}
I am spying on the play method of the audio elements as follows:
mockPlay = jest.spyOn(window.HTMLMediaElement.prototype, 'play')
Potentially there is something I could do here to trigger the ended event?

Safari 14 (macOS). An аudio is truncated at the beginning when repeating [duplicate]

So it turns out, when you use javascript to trigger audio. In the latest version of safari, if you use obj.play() twice, the second time, the part of the audio is cut off (on mac at least). This problem does not occur in Chrome. Only on Safari.
Is anyone aware of a work around for this?
Play
<audio id="t" src="https://biblicaltext.com/audio/%e1%bc%a4.mp3"></audio>
<script>
function playWord(word) {
a = document.getElementById('t');
//a.currentTime = 0
a.play();
//a.src = a.src
}
</script>
https://jsfiddle.net/8fbt7rgc/1/
Redownloading the file using a.src = a.src works, but it is not ideal.
That sounds like a bug they should be made aware of.
For the time being, if you have access to the file you play in a same-origin way, you can use the Web Audio API and its AudioBufferSourceNode interface to play your media with high precision and less latency than through HTMLMediaElements:
(async () => {
const ctx = new (window.AudioContext || window.webkitAudioContext)();
// Using a different file because biblicaltext.com
// doesn't allow cross-origin requests
const data_buf = await fetch("https://dl.dropboxusercontent.com/s/agepbh2agnduknz/camera.mp3")
.then( resp => resp.arrayBuffer() );
const audio_buf = await ctx.decodeAudioData(data_buf);
document.querySelector("a").onclick = (evt) => {
evt.preventDefault();
const source = ctx.createBufferSource();
source.buffer = audio_buf;
source.connect(ctx.destination);
source.start(0);
}
})();
<!-- Promising decodeAudioData for old Safari https://github.com/mohayonao/promise-decode-audio-data/ [MIT] -->
<script src="https://cdn.rawgit.com/mohayonao/promise-decode-audio-data/eb4b1322/build/promise-decode-audio-data.min.js"></script>
Play

JavaScript: Use MediaRecorder to record streams from <video> but failed

I'm trying to record parts of the video from a tag, save it for later use. And I found this article: Recording a media element, which described a method by first calling stream = video.captureStream(), then use new MediaRecord(stream) to get a recorder.
I've tested on some demos, the MediaRecorder works fine if stream is from user's device (such as microphone). However, when it comes to media element, my FireFox browser throws an exception: MediaRecorder.start: The MediaStream's isolation properties disallow access from MediaRecorder.
So any idea on how to deal with it?
Browser: Firefox
The page (including the js file) is stored at local.
The src attribute of <video> tag could either be a file from local storage or a url from Internet.
Code snippets:
let chunks = [];
let getCaptureStream = function () {
let stream;
const fps = 0;
if (video.captureStream) {
console.log("use captureStream");
stream = video.captureStream(fps);
} else if (video.mozCaptureStream) {
console.log("use mozCaptureStream");
stream = video.mozCaptureStream(fps);
} else {
console.error('Stream capture is not supported');
stream = null;
}
return stream;
}
video.addEventListener('play', () => {
let stream = getCaptureStream();
const mediaRecorder = new MediaRecorder(stream);
mediaRecorder.onstop = function() {
const newVideo = document.createElement('video');
newVideo.setAttribute('controls', '');
newVideo.controls = true;
const blob = new Blob(chunks);
chunks = [];
const videoURL = window.URL.createObjectURL(blob, { 'type' : 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"' });
newVideo.src = videoURL;
document.body.appendChild(video);
}
mediaRecorder.ondataavailable = function(e) {
chunks.push(e.data);
}
stopButton.onclick = function() {
mediaRecorder.stop()
}
mediaRecorder.start(); // This is the line triggers exception.
});
I found the solution myself.
When I turned to Chrome, it shows that a CORS issue limits me from even playing original video. So I guess it's because the secure strategy that preventing MediaRecorder from accessing MediaStreams. Therefore, I deployed the local files to a local server with instruction on this page.
After that, the MediaRecorder started working. Hope this will help someone in need.
But still, the official document doesn't seem to mention much about isolation properties of media elements. So any idea or further explanation is welcomed.

Async function doesn't pop off a second time

I'm creating a button to record a canvas using FFMPEG. Here's the code that finalizes the download process.
const recordButton = document.querySelector("#record")
recordButton.addEventListener('click', function () {
function startRecording() {
const { createFFmpeg } = FFmpeg;
const ffmpeg = createFFmpeg({
log: true
});
var transcode = async (webcamData) => {
var name = `record${id}.webm`;
await ffmpeg.load();
await ffmpeg.write(name, webcamData);
await ffmpeg.transcode(name, `output${id}.mp4`);
var data = ffmpeg.read(`output${id}.mp4`);
var video = document.getElementById('output-video');
video.src = URL.createObjectURL(new Blob([data.buffer], { type: 'video/mp4' }));
dl.href = video.src;
}
fn().then(async ({ url, blob }) => {
transcode(new Uint8Array(await (blob).arrayBuffer()));
})
...
id += 1}
The problem arises with the transcode variable. While the button works initially, every other attempt (on a single page load) fails just the async function. I'm not well versed enough in the function to know why it would only work once. That said, I do know it is the only bit of code that does not fire off upon second attempt.
It could be a few things. This is borrowed code, and I've repurposed it for multiple uses. I may have messed up the declarations. It may be an async issue. I tried to use available values to rig up a secondary, similar function, but that would defeat the purpose of the first.
I tried clearing and appending the DOM elements affected, but that doesn't do anything.
It seems to have something to do with:
await ffmpeg.load();
While FFMPEG has to download and load the library initially, it does not have to do so the second initialization. That my be the trigger that is not activating with successive uses.

Assigning different streams to `srcObject` of a video element

I have a video element. I have multiple streams captured by navigator.getUserMedia.
I can assign srcObject successfully the first time:
previewVideoElement.srcObject = stream;
However if I re-assign a different stream to srcObject later (same element) then the stream doesn't work (no errors, blank video). How can I do this without recreating video elements each time?
Edit: trying this fails as well:
const previewVideoElement = document.getElementById("new-device-preview");
previewVideoElement.pause();
previewVideoElement.srcObject = stream;
previewVideoElement.play();
Edit: calling this works a few times, but then fails with The play() request was interrupted by a call to pause(). Without pause I get The play() request was interrupted by a new load request..
previewVideoElement.pause();
previewVideoElement.srcObject = stream;
previewVideoElement.load();
previewVideoElement.play();
Edit: even this heap of garbage doesn't work:
const previewVideoElement = document.getElementById("new-device-preview");
//previewVideoElement.pause();
previewVideoElement.srcObject = stream;
previewVideoElement.load();
const isPlaying = previewVideoElement.currentTime > 0 && !previewVideoElement.paused && !previewVideoElement.ended && previewVideoElement.readyState > 2;
if (!isPlaying)
setTimeout(function () {
previewVideoElement.play();
}, 500);
The only thing I could get working reliably:
var previewVideoElement = document.getElementById("new-device-preview");
if (previewVideoElement.srcObject) {
$("#new-device-preview-container").empty();
$("#new-device-preview-container").html('<video autoplay class="new-device-preview" id="new-device-preview"></video>')
}
previewVideoElement = document.getElementById("new-device-preview");
previewVideoElement.srcObject = stream;

Categories