'How to return the media stream to the UI in a chunks and play audio

I am learning to design a Music streaming app.

In this view, I am trying to list the track details and used HTML audio controls to play the music.

<div class="container">
    <h5>Album Name:</h5>
    @Context.Request.RouteValues["albumName"]
    <ul class="list-group">
        @foreach (var item in Model)
        {
        <li class="list-group-item list-group-item-action justify-content-between align-items-center">
            <span data-track-id="@item.TrackId" class="play-button btn">
                <i class="fa-2x fa-solid fa-circle-play control-icon"></i>
            </span>
            @item.TrackName
            <audio controls>
                <source src='/api/stream/@item.TrackId' type="audio/ogg">
                Your browser does not support the audio element.
            </audio>
        </li>
        }
    </ul>
</div>
[ApiController]
[Route("api")]
public class StreamerController : ControllerBase
{
    private readonly ILogger<StreamerController> _logger;
    private readonly ApplicationDbContext _dbContext;
    public StreamerController(ILogger<StreamerController> logger, ApplicationDbContext dbContext)
    {
        _logger = logger;
        _dbContext = dbContext;
    }

    [HttpGet("stream/{trackId:int}")]
    public async Task<IActionResult> Stream(int trackId)
    {
        byte[] fileData;

        var trackFile = await (from artist in _dbContext.Artists
                               join album in _dbContext.Albums on artist.Id equals album.ArtistId
                               join track in _dbContext.Tracks.Where(al => al.Id == trackId) on album.Id equals track.AlbumId
                               select $"{artist.Name}/{album.DisplayName}".ToLower() + $"/{track.Name}.wav").FirstOrDefaultAsync();

        var filePath = System.IO.Path.Combine(@"wwwroot/uploadfile/", trackFile);

        using (FileStream fs = System.IO.File.OpenRead(filePath))
        {
            using (BinaryReader binaryReader = new BinaryReader(fs))
            {
                fileData = binaryReader.ReadBytes((int)(fs.Length));
            }
        }
        MemoryStream stream = new MemoryStream(fileData);

        return new FileStreamResult(stream, new MediaTypeHeaderValue("audio/mpeg").MediaType)
        {
            EnableRangeProcessing = true
        };
    }
}

This code first grabs the name of the files from the database, then loads the mp3 and returns the FileStreamResult result.

I think I did something wrong while loading the stream and binding it to the audio control because when I load the track details for the first time, it calls the API /api/stream/${trackId} and loads all the audio, and binds it to the audio control, without that audio being played or not.

Also, the database calls are done when the user skips the audio every time to get the path of the audio.

Typically the audio length of the mp3 is more than 1hrs and one mp3 track is of at least 30MB or greater. So loading all the tracks at the same time may create problems in the future.

Actually, I wanted to return the mp3 in chunks and bind it to the audio controls when the file is playing in the audio controls.

But, how it is done properly?

Any suggestion or headstart to improve this code will be appreciated?

enter image description here



Solution 1:[1]

I created new web project to simulate your use case, First I added a sample file audio file into the project, Then I added the following to home controller:

public IActionResult Stream()
{
    var path = Path.Combine(Directory.GetCurrentDirectory(), "FileTest", "audio.mp3");
    var fs = new FileStream(path, FileMode.Open);

    return new FileStreamResult(fs, "audio/mp3")
    {
        EnableRangeProcessing = true,
    };
}

Finally I added the HTML in index as follow :

<audio controls>
    <source src='/home/stream/' type="audio/mp3">
    Your browser does not support the audio element.
</audio>

The audio worked and was in chunks, it didn't transfer all the file.

I think your problem is that you are creating a memory stream and a binary reader which is unnecessary. Just pass the file stream into the file stream result. Also don't put using statement before opening the file stream.

Your code should be something like this:

FileStream stream = new FileStream(filePath, FileMode.Open)

return new FileStreamResult(stream, new "audio/mpeg")
{
    EnableRangeProcessing = true
};

As for the database call, it shouldn't be a problem for low user number. You could use memory cache if the turns out to be a problem. Other than that If there is very big load on the service, the file streaming probably slower, In that case you would need some front facing cache like Nginx but that's another subject.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1