Asp Net Core FileContentResult Audio File to Safari - javascript

In Safari (11), a static audio file loaded into a src via html or javascript works, albeit with the limitations of requiring user input before playing.
ex.
<audio src="static.mp3">
or
var audio = new Audio('static.mp3');
audio.play();
work fine.
However, I need to load audio files from the database, so I was using a controller action like so:
public FileContentResult GetAudio(int audioId)
{
if (!DbContext.Audios.Any(a => a.Id == audioId))
{
return null;
}
var audio = DbContext.Audios.Single(a => a.Id == audioId);
return File(audio.File, "audio/mp3");
}
and set like
<audio src="getaudio?audioId=1">
or
var audio = new Audio('getaudio?audioId=1');
it will not play in MacOS (Safari) or iOS, but works fine in Chrome and Edge (except on iOS). Depending on how I configure things, I get some form of Unhandled Promise error. I've also tried loading into a Web Audio buffer, with the same exact success and failures.
Does anyone have a solution or workaround to load my audio files on Safari?
EDIT
Ok, so on further testing, I discovered that it's not so much whether the files were sent via action or static file, it's how they were saved to the database in the first place. I'm now working to figure out why files I save (as byte[]) and then reload are not recognized by Safari.

OK, so it turns out, I was making the recordings with MediaRecorder, which is a fairly new feature in Chrome and a few other browsers. It didn't matter what format I told it to save as, because only webm is supported. And guess who doesn't support webm format? Safari. Any other browser was picking it up fine, regardless of what incorrect extension I put on the file.
When I find a webm to m4a conversion, I will add it here. I'm sure there are some solutions out there.

Related

Detect if a browser natively supports passing audio to an input tag

I have a webpage that should accept only audio files, more precisely only mp3 files. So I have an input:
<input type="file" accept=".mp3" />
The problem is, Safari on iOS 10.3 ignores the accept attribute. And even if didn't, there still would be no sense to accept audio on iOS 10, because Files are not supported on this iOS version. But what is more confusing, is that Safari shows a modal with these options:
Take Photo or Video
Photo library
Since my website only accepts audio, these options don't make much sense. So I'd prefer to show an error message on this Safari version.
The question is: is there a way (via HTML only or JS) to detect if a browser natively supports passing audio as a file to an <input /> tag?
So, after hours of researching and surfing the Internet, I came to this:
caniuse states that almost no mobile browser supports the accept attribute. Therefore, you cannot prevent mobile browsers from selecting file types different from desirable
there are similar questions on SO where the answer is the same: write your own input handler, check the file extension, and show an error if it's not the one you'd like
I guess, this approach does make sense. However, not all mp3 files actually have an .mp3 suffix in its filename (which probably is a bizzare case, but OSes like Android let you do that). So I chose to test the file via Web Audio API:
// inside the input handler
const fr = new FileReader();
fr.readAsArrayBuffer(file);
fr.addEventListener('loadend', () => {
audioCtx.current = new AudioContext();
audioCtx.current.decodeAudioData(
fr.result,
successCallback,
(err) => {
alert('Incorrect file format. Please, pass an audio file');
},
);
My page does some visualization so it decodes an audio anyway. However, with this approach I can only test that the file is a correct audio, but not that it's an exactly mp3 file.

WebAudioAPI decodeAudioData() giving null error on iOS 14 Safari

I have an mp3 audio stream player that works well in every desktop browser, using MediaSourceExtensions with a fallback to WebAudioAPI for those browsers that do not support MSE. iOS Safari is one such browser, and should theoretically support mp3 decoding via the Web Audio API without issues.
I've been struggling to get iOS Safari to properly play the mp3 audio chunks that are being returned from the stream. So far, it's the only browser that seems to have issues and I can't for the life of me figure out what's going on. Sadly, there isn't a whole lot of information on corner cases like this and the other questions here on StackOverflow haven't been any help.
Here's the relevant part of my js code where things are getting hung up. It's a callback function for an async fetch() process that's grabbing the mp3 data from the stream.
async function pushStream(value) {
// Web Audio streaming for browsers that don't support MSE
if (usingWebAudio) {
// convert the stream UInt8Array to an ArrayBuffer
var dataBuffer = value.stream.buffer;
// decode the raw mp3 chunks
audCtx.decodeAudioData(dataBuffer, function(newData) {
// add the decoded data to the buffer
console.log("pushing new audio data to buffer");
webAudioBuffer.push(newData);
// if we have audio in the buffer, play it
if (webAudioBuffer.length) {
scheduleWebAudio();
}
}, function(e) {
console.error(e);
});
What I'm seeing is the error callback being fired and printing null: null as its error message (very helpful). Every so often, I will see the console print pushing new audio data to buffer, but this seems to only happen about once every few minutes while the stream is playing. Almost all the stream data is erroring out during the decode and the lack of useful error messages is preventing me from figuring out why.
As far as I can tell, iOS safari should support mp3 streams without any issues. It also should support the decodeAudioData() function. Most of the other answers I was able to find related to trying to play audio before the user interacts with the screen. In my case, I start the audio using a play button on the page so I don't believe that's the problem either.
One final thing, I'm developing on Windows and using the remotedebug iOS adapter. This could possibly be the reason why I'm not getting useful debug messages, however all other debug and error prints seem to work fine so I don't believe that's the case.
Thanks in advance for any help!
Unfortunately there is a bug in Safari which causes it to reject the decodeAudioData() promise with null. From my experience this happens in cases where it should actually reject the promise with an EncodingError instead.
The bug can be reproduced by asking Safari do decode an image. https://github.com/chrisguttandin/standardized-audio-context/blob/9c705bd2e5d8a774b93b07c3b203c8f343737988/test/expectation/safari/any/offline-audio-context-constructor.js#L648-L663
In general decodeAudioData() can only handle full files. It isn't capable of decoding a file in chunks. The WebCodecs API is meant to solve that but I guess it won't be available on iOS anytime soon.
However there is one trick that works with MP3s because of their internal structure. MP3s are built out of chunks themselves and any number of those chunks form a technically valid MP3. That means you can pre-process your data by making sure that each of the buffers that you pass on to decodeAudioData() begins and ends exactly at those internal chunk boundaries. The phonograph library does for example follow that principle.

How to use MediaRecorder as MediaSource

As an exercise in learning WebRTC I am trying to to show the local webcam and side by side with a delayed playback of the webcam. In order to achieve this I am trying to pass recorded blobs to a BufferSource and use the corresponding MediaSource as source for a video element.
// the ondataavailable callback for the MediaRecorder
async function handleDataAvailable(event) {
// console.log("handleDataAvailable", event);
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
if (recordedBlobs.length > 5) {
if (recordedBlobs.length === 5)
console.log("buffered enough for delayed playback");
if (!updatingBuffer) {
updatingBuffer = true;
const bufferedBlob = recordedBlobs.shift();
const bufferedAsArrayBuffer = await bufferedBlob.arrayBuffer();
if (!sourceBuffer.updating) {
console.log("appending to buffer");
sourceBuffer.appendBuffer(bufferedAsArrayBuffer);
} else {
console.warn("Buffer still updating... ");
recordedBlobs.unshift(bufferedBlob);
}
}
}
}
// connecting the media source to the video element
recordedVideo.src = null;
recordedVideo.srcObject = null;
recordedVideo.src = window.URL.createObjectURL(mediaSource);
recordedVideo.controls = true;
try {
await recordedVideo.play();
} catch (e) {
console.error(`Play failed: ${e}`);
}
All code: https://jsfiddle.net/43rm7258/1/
When I run this in Chromium 78 I get an NotSupportedError: Failed to load because no supported source was found. from the play element of the video element.
I have no clue what I am doing wrong or how to proceed at this point.
This is about something similar, but does not help me: MediaSource randomly stops video
This example was my starting point: https://webrtc.github.io/samples/src/content/getusermedia/record/
In Summary
Getting it to work in Firefox and Chrome is easy: you just need to add an audio codec to your codecs list! video/webm;codecs=opus,vp8
Getting it to work in Safari is significantly more complicated. MediaRecorder is an "experimental" feature that has to be manually enabled under the developer options. Once enabled, Safari lacks an isTypeSupported method, so you need to handle that. Finally, no matter what you request from the MediaRecorder, Safari will always hand you an MP4 file - which cannot be streamed the way WEBM can. This means you need to perform transmuxing in JavaScript to convert the video container format on the fly
Android should work if Chrome works
iOS does not support Media Source Extensions, so SourceBuffer is not defined on iOS and the whole solution will not work
Original Post
Looking at the JSFiddle you posted, one quick fix before we get started:
You reference a variable errorMsgElement which is never defined. You should add a <div> to the page with an appropriate ID and then create a const errorMsgElement = document.querySelector(...) line to capture it
Now something to note when working with Media Source Extensions and MediaRecorder is that support is going to be very different per browser. Even though this is a "standardized" part of the HTML5 spec, it's not very consistent across platforms. In my experience getting MediaRecorder to work in Firefox doesn't take too much effort, getting it to work in Chrome is a bit harder, getting it to work in Safari is damned-near impossible, and getting it to work on iOS is literally not something you can do.
I went through and debugged this on a per-browser basis and recorded my steps, so that you can understand some of the tools available to you when debugging media issues
Firefox
When I checked out your JSFiddle in Firefox, I saw the following error in the console:
NotSupportedError: An audio track cannot be recorded: video/webm;codecs=vp8 indicates an unsupported codec
I recall that VP8 / VP9 were big pushes by Google and as such may not work in Firefox, so I tried making one small tweak to your code. I removed the , options) parameter from your call to new MediaRecorder(). This tells the browser to use whatever codec it wants, so you will likely get different output in every browser (but it should at least work in every browser)
This worked in Firefox, so I checked out Chrome.
Chrome
This time I got a new error:
(index):409 Uncaught (in promise) DOMException: Failed to execute 'appendBuffer' on 'SourceBuffer': This SourceBuffer has been removed from the parent media source.
at MediaRecorder.handleDataAvailable (https://fiddle.jshell.net/43rm7258/1/show/:409:22)
So I headed over to chrome://media-internals/ in my browser and saw this:
Audio stream codec opus doesn't match SourceBuffer codecs.
In your code you're specifying a video codec (VP9 or VP8) but not an audio codec, so the MediaRecorder is letting the browser choose any audio codec it wants. It looks like in Chrome's MediaRecorder by default chooses "opus" as the audio codec, but Chrome's SourceBuffer by default chooses something else. This was trivially fixed. I updated your two lines that set the options.mimeType like so:
options = { mimeType: "video/webm;codecs=opus, vp9" };
options = { mimeType: "video/webm;codecs=opus, vp8" };
Since you use the same options object for declaring the MediaRecorder and the SourceBuffer, adding the audio codec to the list means the SourceBuffer is now declared with a valid audio codec and the video plays
For good measure I tested the new code (with an audio codec) on Firefox. This worked! So we're 2 for 2 just by adding the audio codec to the options list (and leaving it in the parameters for declaring the MediaRecorder)
It looks like VP8 and opus work in Firefox, but aren't the defaults (although unlike Chrome, the default for MediaRecorder and SourceBuffer are the same, which is why removing the options parameter entirely worked)
Safari
This time we got an error that we may not be able to work through:
Unhandled Promise Rejection: ReferenceError: Can't find variable: MediaRecorder
The first thing I did was Google "Safari MediaRecorder", which turned up this article. I thought I'd give it a try, so I took a look. Sure enough:
I clicked on this to enable MediaRecorder and was met with the following in the console:
Unhandled Promise Rejection: TypeError: MediaRecorder.isTypeSupported is not a function. (In 'MediaRecorder.isTypeSupported(options.mimeType)', 'MediaRecorder.isTypeSupported' is undefined)
So Safari doesn't have the isTypeSupported method. Not to worry, we'll just say "if this method doesn't exist, assume it's Safari and set the type accordingly"
if (MediaRecorder.isTypeSupported) {
options = { mimeType: "video/webm;codecs=vp9" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not Supported`);
errorMsgElement.innerHTML = `${options.mimeType} is not Supported`;
options = { mimeType: "video/webm;codecs=vp8" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not Supported`);
errorMsgElement.innerHTML = `${options.mimeType} is not Supported`;
options = { mimeType: "video/webm" };
if (!MediaRecorder.isTypeSupported(options.mimeType)) {
console.error(`${options.mimeType} is not Supported`);
errorMsgElement.innerHTML = `${options.mimeType} is not Supported`;
options = { mimeType: "" };
}
}
}
} else {
options = { mimeType: "" };
}
Now I just had to find a mimeType that Safari supported. Some light Googling suggests that H.264 is supported, so I tried:
options = { mimeType: "video/webm;codecs=h264" };
This successfully gave me MediaRecorder started, but failed at the line addSourceBuffer with the new error:
NotSupportedError: The operation is not supported.
I will continue to try and diagnose how to get this working in Safari, but for now I've at least addresses Firefox and Chrome
Update 1
I've continued to work on Safari. Unfortunately Safari lacks the tooling of Chrome and Firefox to dig deep into media internals, so there's a lot of guesswork involved.
I had previously figured out that we were getting an error "The operation is not supported" when trying to call addSourceBuffer. So I created a one-off page to try and call just this method under different circumstances:
Maybe add a source buffer before play is called on the video
Maybe add a source buffer before the media source has been attached to a video element
Maybe add a source buffer with different codecs
etc
I found that the issue was the codec still, and that the error messaging about the "operation" not being permitted was slightly misleading. It was the parameters that were not permitted. Simply supplying "h264" worked for the MediaRecorder, but the SourceBuffer needed me to pass along the codec parameters.
One of the first things I tried was heading to the MDN sample page and copying the codecs they used there: 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"'. This gave the same "operation not permitted" error. Digging into the meaning of these codec parameters (like what the heck does 42E01E even mean?). While I wish I had a better answer, while Googling it I stumbled upon this StackOverflow post which mentioned using 'video/mp4; codecs="avc1.64000d,mp4a.40.2"' on Safari. I gave it a try and the console errors were gone!
Although the console errors are gone now, I'm still not seeing any video. So there is still work to do.
Update 2
Further investigation in the Debugger in Safari (placing multiple breakpoints and inspecting variables at each step of the process) found that handleDataAvailable was never being called in Safari. It looks like in Firefox and Chrome mediaRecorder.start(100) will properly follow the spec and call ondatavailable every 100 milliseconds, but Safari ignores the parameter and buffers everything into one massive Blob. Calling mediaRecorder.stop() manually caused ondataavailable to be called with everything that had been recorded up until that point
I tried using setInterval to call mediaRecorder.requestData() every 100 milliseconds, but requestData was not defined in Safari (much like how isTypeSupported was not defined). This put me in a bit of a bind.
Next I tried cleaning up the whole MediaRecorder object and creating a new one every 100 milliseconds, but this threw an error on the line await bufferedBlob.arrayBuffer(). I'm still investigating why that one failed
Update 3
One thing I recall about the MP4 format is that the "moov" atom is required in order to play back any content. This is why you can't download the middle half of an MP4 file and play it. You need to download the WHOLE file. So I wondered if the fact that I selected MP4 was the reason I was not getting regular updates.
I tried changing video/mp4 to a few different values and got varying results:
video/webm -- Operation is not supported
video/x-m4v -- Behaved like MP4, I only got data when .stop() was called
video/3gpp -- Behaved like MP4
video/flv -- Operation is not supported
video/mpeg -- Behaved like MP4
Everything behaving like MP4 led me to inspect the data that was actually being passed to handleDataAvailable. That's when I noticed this:
No matter what I selected for the video format, Safari was always giving me an MP4!
Suddenly I remembered why Safari was such a nightmare, and why I had mentally classified it as "damned-near impossible". In order to stitch together several MP4s would require a JavaScript transmuxer
That's when I remembered, that's exactly what I had done before. I worked with MediaRecorder and SourceBuffer just over a year ago to try and create a JavaScript RTMP player. Once the player was done, I wanted to add support for DVR (seeking back to parts of the video that had already been streamed), which I did by using MediaRecorder and keeping a ring buffer in memory of 1-second video blobs. On Safari I ran these video blobs through the transmuxer I had coded to convert them from MP4 to ISO-BMFF so I could concatenate them together.
I wish I could share the code with you, but it's all owned by my old employer - so at this point the solution has been lost to me. I know that someone went through the trouble of compiling FFMPEG to JavaScript using emscripten, so you may be able to take advantage of that.
Additionally, I suffered issues with MediaRecorder. At the time of Audio Record, Mime Types are different like
Mac chrome - Mime Type:audio/webm;codecs=opus
Mac Safari - Mime Type:audio/mp4
Windows/Android - Mime Type:audio/webm;codecs=opus
Iphone Chrome - Mime Type:audio/mp4
In PC, I was saving the file as M4a but Audio was not running in IOS. After some analysis and Testing. I decided to convert the file after Upload in Server and used ffmpeg and It worked like a charm.
<!-- https://mvnrepository.com/artifact/org.bytedeco/ffmpeg-platform -->
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>4.3.2-1.5.5</version>
</dependency>
/**
* Convert the file into MP4 using H264 Codac in order to make it work in IOS Mobile Device
* #param file
* #param outputFile
*/
private void convertToM4A(File file, File outputFile) {
try {
String ffmpeg = Loader.load(org.bytedeco.ffmpeg.ffmpeg.class);
ProcessBuilder pb = new ProcessBuilder(ffmpeg, "-i", file.getPath(), "-vcodec", "h264", outputFile.getPath());
pb.inheritIO().start().waitFor();
}catch (Exception e ){
e.printStackTrace();
}
}

HTML5 Game - Works in IE, Chrome. Fails in FireFox...Until I Navigate to a .mp3 File...Then It Works

I'm using a game library (http://html5quintus.com/) to try and make a very simple 2d platformer. I don't think this is necessarily specific to game development though. I'm really just trying to play some audio files.
I'm using FireFox 31.0 - (the latest AFAIK).
The audio files I'm dealing with are all .mp3 files. The line of code that seems to be causing the problem is:
Q.audioContext.decodeAudioData(request.response, function(buffer) {
callback(key,buffer);
}, errorCallback);
The error I get is:
The buffer passed to decodeAudioData contains invalid content which
cannot be decoded successfully. localhost : 50796 uncaught exception:
Error Loading: jump.mp3
What I find most confusing though, is that if I navigate to the audio file directly (/audio/jump.mp3) - FireFox will play that audio file correctly.
Here's where it gets really weird....AFTER I load any one of the .mp3 files. If I navigate back to my game and reload it - it suddenly works completely. All of the sounds/background music work and everything is just like it is in IE and Chrome. But if I don't manually load a .mp3 file first, it doesn't.
Once the game is working, I can close that tab/open a new tab and visit the URL and it works again. It seems to continue working fine until I exit FireFox completely. Then, the game won't work until I first revisit the .mp3 file directly.
Can anyone tell me what I've done wrong here?
In my project the decoding worked fine for months until this morning.
So it's not the audio files, it's not a caching issue, this error only
appears in Firefox (I have v31.0), in other browsers(both desktop and mobile) it works fine.
So I'd bet that it's a Firefox bug, one that unfortunately I don't have a solution for yet.
Will update if I find something.
As a work-around (for FireFox 31); if I add a regular audio tag to my static loading page, when the game engine gets around to loading it's resources, FireFox will accept the .mp3 files.
<audio src="audio/coin.mp3"></audio>
If I include that line everything works on all of the major browsers. If I remove that line, FireFox 30 works, but FireFox 31 fails :( Yuck.

HTML5 MP3 <audio> not loading correctly

Ok, my new website has just gone live, delivered through Google Apps. On a lark, I decided to include a javascript / HTML5 Lunar Lander clone (Martian Lander) which I wrote as an exercise a while back. The game works fine when I open it locally, but when it's delivered through GAE, the sounds don't seem to load on every system. In mobile safari, none of them load. In safari on the desktop, they all load reliably on my computer, but not on some other computers. In Chrome (on the desktop) it seems to work, but in Chrome in iOS, only one sound loads. On the desktop, it always seems to be the same sound which fails to load (explode1.mp3), which is the smallest of the sounds I'm loading. As you can see, if you click that link, the sound downloads fine from the server...
At first the problem seemed to be related to case sensitivity, so I switched the case in the filename, but that fix didn't keep working. This is a problem, as my loading bar is directly tied to how many resources have loaded, so it just sits there waiting for a GET request with no reply... Has anyone experienced anything like this, where a GET receives no reply on a specific resource, but loading the resource directly works fine?
I should say that I'm very new to most of these technologies, so it seems quite likely to me that I just made some novice mistake. Unfortunately, I'm not sure what those novice mistakes would be, seeing as I'm a novice!
Here's the code I use to load the sounds:
function loadSound(soundName) {
var newElement = document.createElement("audio");
newElement.addEventListener("canplaythrough", assetLoaded, false);
document.body.appendChild(newElement);
var audioType = supportedAudioFormat(newElement);
if (audioType == "") {
alert("no audio support");
return;
}
newElement.setAttribute("src", "lander/sounds/" + soundName + "." + audioType);
console.log("loading sound " + newElement.src + "...");
return newElement;
}
and...
function assetLoaded() {
var assetName = this.src;
numAssetsLoaded++;
console.log("loaded asset " + numAssetsLoaded + " (" + assetName + ")");
if (numAssetsLoaded >= numAssetsToLoad) {
shipSpriteSheet.removeEventListener("load", assetLoaded, false);
pointImage.removeEventListener("load", assetLoaded, false);
thrustAudioElement.removeEventListener("canplaythrough", assetLoaded, false);
explosionAudioElement.removeEventListener("canplaythrough", assetLoaded, false);
victoryAudioElement.removeEventListener("canplaythrough", assetLoaded, false);
musicTrackElement.removeEventListener("canplaythrough", assetLoaded, false);
gameState = GAME_STATE_INIT;
}
}
If you take a look at the console output, you'll see that all of the sounds begin loading (particularly explode1.mp3) but don't necessarily finish and call assetLoaded...
UPDATE:
It seems to be the consensus is that I should not be using mp3 (incidentally, I'm already using mp3, AAC, AND ogg, but defaulting to mp3), and also that I should use the Web Audio API. These are both welcome pieces of input, and I will make the necessary changes. However, I still don't have an answer to the original question, which is, "Why does one particular sound not load reliably on desktop while the others load with no problem?" Anybody wanna take a crack at that one? Or is the answer going to be something like, "These things are highly unpredictable, and there's no way to fix it except by switching to a more dependable methodology, like Web Audio API"?
UDATE:
Here's an excerpt from my app.yaml file, which, I gather, helps GAE setup the server.
- url: /(.*\.(mp3|ogg|wav))
static_files: \1
upload: (.*\.(mp3|ogg|wav))
Some things to be aware of:
You shouldn't use MP3 for HTML5 games.
You will need to dual-encode all your sounds to both AAC (.m4a) and Ogg Vorbis (.ogg) to ensure they can be played everywhere, since there is no one format which can be played everywhere.
You must ensure your server has the correct MIME types for the audio files. Some browsers will happily play audio if the server says it has the wrong MIME type; others will fail silently. For AAC and Ogg Vorbis the types are audio/mp4 and audio/ogg respectively.
Most mobile devices can only play one sound at a time, and iOS generally doesn't let you play audio unless it's in a user-initiated input event (such as touchstart).
You'll probably want to use the Web Audio API where supported (Chrome and iOS 6+) since playback is more reliable and polyphonic even on iOS - but note iOS still mutes the Web Audio API until a user input event.
This is not a direct answer to your question why sound is not being played, but more like what you should do with your game sound effects.
For game sound effects I suggest you use HTML5 Web Audio API which gives more control over how sounds are played (pitch of the sound effect, less delay in playback, etc):
http://www.html5rocks.com/en/tutorials/webaudio/intro/
iOS 6+ supports Web Audio https://developer.apple.com/technologies/ios6/
Web audio is not supported in FF yet, but the support is coming

Categories

Resources