Search code examples
c#winformslibvlclibvlcsharp

How do I fetch artwork from streamed MP3 media?


I am using libVLCSharp to play audio and video media in a Winforms app. I am able to fetch embedded cover art from MP3 files by calling this PlayMediaFile() method with a FileInfo pointing to an MP3 with artwork:`

    //  media & artwork  must be disposed after playing media 
    async Task PlayMediaFile(FileInfo mediaFI) {
        var media = new Media(LibVlc, mediaFI.FullName, FromType.FromPath);
        await media.Parse();
        Play(media);
    }

    void Play(Media media) {
        var artworkPath = media.Meta(MetadataType.ArtworkURL);
        Bitmap? artwork = null;
        if (artworkPath is not null) {
            var artworkUri = new Uri(artworkPath);
            var artworkFI = new FileInfo(artworkUri.LocalPath);
            artwork = artworkFI.Exists ? new Bitmap(artworkFI.FullName) : null;
        }
        MediaPlayer.Media = media;
        videoView.BackgroundImage = artwork;
        MediaPlayer.Play();
    }

`

However, if I try to play an MP3 stream, using:`

    //  stream, media, mediaInput & artwork  must be disposed after playing media 
    async Task PlayMediaStream(Stream stream) {
        var mediaInput = new StreamMediaInput(stream);
        var media = new Media(LibVlc, mediaInput);
        await media.Parse(MediaParseOptions.ParseLocal); // and combinations with MediaParseOptions.ParseNetwork, MediaParseOptions.FetchNetwork - none worked
        Play(media);
    }

` The Artwork is always null. I know the stream has artwork; I can save it to file and fetch it using my 'PlayMediaFile()' method. Is there any way to fetch artwork from an MP3 media stream (short of writing it to a temp file and loading that)?


Solution

  • After taking a look at the source code VideoLan VLC repository, more specifically the preparser.c file, it appears that this behavior is intentional.
    When the preparser is preparing the tasks for the callbacks (see the vlc_preparser_Push() function, the options (map to the MediaParseOptions) are evaluated.
    If the current Media Item is not a Local File, Directory or PlayList, then the parsing is skipped (sets an ITEM_PREPARSE_SKIPPED flag) and returns success immediately.

    This is presumably related to an implementation choice that considers a fast reaction to streaming request more important than buffering enough to get to the section where the metadata is stored.

    Of course, this happens because the MediaInput abstract class and the derived StreamMediaInput behave like this.
    Nothing stops you from deriving a custom stream handler from MediaInput and provide an alternative buffering behavior, keeping track of the current Read position and buffering further to complete the stream and get the metadata way before the playback is completed.

    Then parse the metadata and notify, e.g., via events, that the parsing is complete.
    Easy wording it, not so simple in practice. Requires a lot of testing.

    I've teste all combinations of MediaParseOptions anyway, to see what happens (because this is the LibVLCSharp implementation), but the ParsedStatus is always MediaParsedStatus.Skipped, as the source code suggests it would be.

    I've changed the code a bit while testing, to be more IDisposable compliant. Further testing in this department is needed, because of the fire-and-forget nature of the MediaPlayer's functionality.
    More specifically, the StreamMediaInput and Media object must be disposed before a new stream is played. The current implementation doesn't do that (no finalizers either)

    Image? Artwork { get; set; }
    LibVLC libVlc = new LibVLC();
    MediaPlayer? mediaPlayer = null;
    
    async Task PlayMediaStream(Stream stream) {
        mediaPlayer?.Dispose();
        mediaPlayer = new MediaPlayer(libVlc);
        Artwork?.Dispose();
        Artwork = null;
    
        // Both StreamMediaInput and Media must be disposed when the playback ends
        var mediaInput = new StreamMediaInput(stream);
        var media = new Media(libVlc, mediaInput);
    
        media.ParsedChanged += (s, a) => {
            if (a.ParsedStatus == MediaParsedStatus.Done) {
                var artworkPath = media.Meta(MetadataType.ArtworkURL);
                if (artworkPath is not null) {
                    var artworkFI = new FileInfo(new Uri(artworkPath).LocalPath);
                    if (artworkFI.Exists) {
                        Artwork = new Bitmap(artworkFI.FullName);
                    }
                }
            }
            videoView.BackgroundImage = Artwork;
        };
    
        // Whatever combination 
        await media.Parse(MediaParseOptions.FetchLocal | MediaParseOptions.ParseLocal | 
                          MediaParseOptions.FetchNetwork | MediaParseOptions.ParseNetwork);
        mediaPlayer.Media = media;
        var success = mediaPlayer.Play();
    }