I am creating a Blazor project v0.5.1 that is using a .NET Standard library to provided all of the business logic. This library has several WAV files stored as Embedded Resources.
Consuming one of the resources in a typical .NET technology (WinForms, WPF, etc.) that reference this library can be done with this:
var assemName = "MyLibName";
var assembly = AppDomain
.CurrentDomain
.GetAssemblies()
.First(a => a.GetName().Name == assemName);
//memory stream
var stream = assembly.GetManifestResourceStream($"{assemName}.mysound.wav");
//Can play the file at some point later
var player = new SoundPlayer(stream)
player.Play();
I would like to do the equivalent in Blazor. Right now, I have sounds working in the app by copy/pasting the wav files into wwwroot\sounds
folder of the project so it is served as static HTML content. Then, in JavaScript, I can play one like this:
const audio = new Audio"\sounds\mysound.wave"]);
audio.currentTime = 0;
audio.play();
But what I would really like to do is avoid the copy-paste and somehow serve out the files dynamically as those endpoints so it is transparent to JS.
Well, I was able to get it. Not sure it if the the best way but it seems to work well.
I had to add the blazor Storage extension:
https://github.com/BlazorExtensions/Storage
which acts as a proxy to the JavaScript SessionStorage and LocalStorage from .NET. Once loaded, I added each embedded wav file from .NET like this:
foreach (var kvp in SoundStreamDictionary)
{
await sessionStorage.SetItem(
kvp.Key.ToString().ToLower() //Key is the name of the sound
, kvp.Value.ToBase64() //Value is a Stream object
);
}
The ToBase64
is pretty standard .NET stream manipulation:
public static string ToBase64(this Stream stream)
{
byte[] bytes;
using (var memoryStream = new MemoryStream())
{
stream.Position = 0;
stream.CopyTo(memoryStream);
bytes = memoryStream.ToArray();
}
return Convert.ToBase64String(bytes);
}
Now all data is stored in JavaScript SessionStorage as strings. Trick is now to decode that to audio. With the help of this JS helper method (thanks to this StackOverflow post):
function b64toBlob(b64Data, contentType, sliceSize) {
contentType = contentType || '';
sliceSize = sliceSize || 512;
const byteCharacters = atob(b64Data);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
byteArrays.push(byteArray);
}
return new Blob(byteArrays, { type: contentType });
}
Put that and this in the JsInterop.js file of the BlazorComponent:
const soundAudios = [];
window.JsSound = {
loadSounds: function (sounds) {
sounds.forEach(sound => {
//Blazor.Storage extension adds extra double quotes so trim
const str64 = sessionStorage.getItem(sound.path).slice(1, -1);
const blob = b64toBlob(str64, "audio/wav");
const blobUrl = URL.createObjectURL(blob);
const audio = new Audio(blobUrl);
soundAudios[sound.id] = audio;
});
return true;
},
play: function (id) {
const audio = soundAudios[id];
audio.currentTime = 0;
audio.play();
return true;
},
};
And FINALLY the sounds can be invoked by name from .Net:
private IDictionary<string, int> soundDict = new Dictionary<string, int>();
public Task<string> LoadSounds(IEnumerable<string> fileNames)
{
var sounds = fileNames
.Select(name =>
{
var snd = new
{
id = soundDict.Count,
path = name
};
soundDict.Add(name, snd.id);
return snd;
})
.ToList();
return JSRuntime.Current.InvokeAsync<string>(
"JsSound.loadSounds"
, sounds
);
}
public Task<string> Play(string name)
{
return JSRuntime.Current.InvokeAsync<string>(
"JsSound.play"
, soundDict[name]
);
}