C# LibVLCSharp切换媒体流导致HTTP异常

C# LibVLCSharp switching media stream causes HTTP exception

我们正在使用 to play a live mp3 network stream in our 应用,使用以下代码片段

public bool Play(Uri uri)
{
    Media newMedia = new(this.LibVLC, uri);
    Media? oldMedia = this.VLCMediaPlayer.Media;
    bool success = this.VLCMediaPlayer.Play(newMedia);
    oldMedia?.Dispose();
    return success;
}

其中 VLCMediaPlayer 是 VLC MediaPlayer class 的一个实例。上面的代码有效,流式传输完美无缺。

然而:

在某些时候我们需要切换流(即原始实时 mp3 流继续/在技术上没有结束,我们需要停止流式传输并切换到另一个流)。

就文档而言,似乎只是在不同的 URL 上调用 Play() 应该 就可以了。问题是,事实并非如此。原始流停止几毫秒,然后继续。

我们的代码看起来像这样:

// use our custom Play() wrapper
Play(new Uri("https://whatever.com/some-live-stream.mp3"));

// ... do other stuff

// at some time later switch to a different stream using the same Play() wrapper
Play(new Uri("https://whatever.com/file-stream.mp3"));

问题:

VLC 没有开始播放 file-stream.mp3,而是挂起几毫秒,然后继续播放 some-live-stream.mp3。为什么会这样,我们该如何解决?

更新(似乎是一个集成错误):

运行 带有调试输出的 libVLC 揭示了这一点:

[00000177e9c21830] main input debug: Creating an input for 'file-stream.mp3'
[00000177e9c21830]playing uri
 main input debug: using timeshift granularity of 50 MiB
[00000177e9c21830] main input debug: using timeshift path: C:\Users\EXPUNGED\AppData\Local\Temp
[00000177e9c21830] main input debug: `http://127.0.0.1:5050/file-stream.mp3' gives access `http' demux `any' path `127.0.0.1:5050/file-stream.mp3'
[00000177e9b29350] main input source debug: creating demux: access='http' demux='any' location='127.0.0.1:5050/file-stream.mp3' file='\127.0.0.1:5050\file-stream.mp3'
[00000177e92e1d60] main demux debug: looking for access_demux module matching "http": 15 candidates
[00000177e92e1d60] main demux debug: no access_demux modules matched
[00000177e904e6d0] main stream debug: creating access: http://127.0.0.1:5050/file-stream.mp3
[00000177e904e6d0] main stream debug:  (path: \127.0.0.1:5050\file-stream.mp3)
[00000177e904e6d0] main stream debug: looking for access module matching "http": 27 candidates
[00000177e904e6d0] http stream debug: resolving 127.0.0.1 ...
[00000177e904e6d0] http stream debug: outgoing request:
GET /file-stream.mp3 HTTP/1.1
Host: 127.0.0.1:5050
Accept: */*
Accept-Language: en_US
User-Agent: VLC/3.0.16 LibVLC/3.0.16
Range: bytes=0-


[00000177e904e6d0] http stream debug: connection failed 
[00000177e904e6d0] access stream error: HTTP connection failure
[00000177e904e6d0] http stream debug: querying proxy for http://127.0.0.1:5050/file-stream.mp3
[00000177e904e6d0] http stream debug: no proxy
[00000177e904e6d0] http stream debug: http: server='127.0.0.1' port=5050 file='/file-stream.mp3'
[00000177e904e6d0] main stream debug: net: connecting to 127.0.0.1 port 5050
[00000177e904e6d0] http stream error: cannot connect to 127.0.0.1:5050
[00000177e904e6d0] main stream debug: no access modules matched

(上面的片段从调用第二个 Play() 时开始。)

立即引起注意的是HTTP connection failure

不过,让我扩展一下我们最小的可重现示例。在生产中,我们不仅需要切换一次流,还需要多次。我们停止第一个 some-live-stream.mp3 流(它运行 24/7 服务器端)。然后我们需要切换到 file-stream.mp3 这是一个大约 10 秒长的 mp3 文件。播放文件后,我们将继续播放第一个流 (some-live-stream.mp3).

因此,除了最初包含在这个问题中的 Play() 方法之外,我们还编写了一个自定义 Enqueue() 方法。

我们内部 VLC 包装器 class 的整个 相关 代码实际上看起来像这样:

public sealed class MediaService
{
    private readonly LibVLC _libVLC;
    private readonly ConcurrentQueue<Uri> _playlist = new();

    public MediaPlayer VLCPlayer { get; }

    internal MediaService(LibVLC libVLC)
    {
        _libVLC = libVLC;
        VLCPlayer = new MediaPlayer(_libVLC);
        VLCPlayer.EndReached += VLCPlayer_EndReached;
    }
    
    public bool Play(Uri uri)
    {
        Media newMedia = new(_libVLC, uri);
        Media? oldMedia = VLCPlayer.Media;
        bool success = VLCPlayer.Play(newMedia);
        oldMedia?.Dispose();
        CurrentUrl = uri.AbsoluteUri;
        return success;
    }

    public bool IsStartingOrPlaying() =>
        VLCPlayer.State is VLCState.Buffering
        or VLCState.Opening
        or VLCState.Playing;
        
    public void Enqueue(Uri uri)
    {
        if (IsStartingOrPlaying())
        {
            _playlist.Enqueue(uri);
        }
        else
        {
            Play(uri);
        }
    }

    private void VLCPlayer_EndReached(object sender, EventArgs e)
    {
        if (_playlist.TryDequeue(out Uri? result))
        {
            // don't deadlock on VLC callback
            Task.Run(() => Play(result));
        }
    }
}

现在来看最小的可重现示例:

下面的代码失败(即导致 HTTP 连接失败)

using our.vlc.wrapper;

CoreLoader.Initialize(true);
MediaService mediaService = MediaServiceFactory.GetSharedInstance();

// start streaming the live stream
Task.Run(() => mediaService.Play("http://127.0.0.1:5050/some-live-stream.mp3"))
    // do other stuff...
    .ContinueWith((_) => Thread.Sleep(10000)) 
    // now interrupt the original stream with the mp3 file
    .ContinueWith((_) => mediaService.Play("http://127.0.0.1:5050/file-stream.mp3"))
    // and continue the live stream after mp3 file stream ends
    .ContinueWith((_) => mediaService.Enqueue("http://127.0.0.1:5050/some-live-stream.mp3"));

Console.ReadLine();

尝试流式传输时的 HTTP 异常 http://127.0.0.1:5050/file-stream.mp3 显然会导致原始问题中描述的“滞后”,之后原始流将按预期继续。

然而 此代码片段有效:

using our.vlc.wrapper;

CoreLoader.Initialize(true);
MediaService mediaService = MediaServiceFactory.GetSharedInstance();

// start streaming the live stream
Task.Run(() => mediaService.Play("http://127.0.0.1:5050/some-live-stream.mp3"))
    // do other stuff...
    .ContinueWith((_) => Thread.Sleep(10000)) 
    // now interrupt the original stream with the mp3 file
    .ContinueWith((_) => mediaService.Play("http://127.0.0.1:5050/file-stream.mp3"))
    // another sleep seems to cause no connection failure
    .ContinueWith((_) => Thread.Sleep(10000))
    // call PLAY() instead of ENQUEUE()
    .ContinueWith((_) => mediaService.Play("http://127.0.0.1:5050/some-live-stream.mp3"));

Console.ReadLine();

所以实际上我们的 Enqueue() 方法似乎是罪魁祸首。但是,我不明白为什么它的实现会成为一个问题并导致 看似随机的 HTTP 连接失败,当 /file-stream.mp3 端点经过良好测试甚至在第二个示例中工作时。

使用某种排队对我们的项目至关重要,因此我们不能像在最小可重现示例中那样退回到 Thread.Sleep() 调用。我们如何修复我们的 Enqueue() 方法,为什么它没有按预期工作?

更新 2

使用 Wireshark 检查“HTTP 连接失败”期间的流量显示:

我们可以看到第一个 /some-live-stream.mp3 的终止 TCP 连接的 FINFIN-ACKACKSYNSYN-ACK, ACK 的 HTTP 请求到 /file-stream.mp3 所以它有点难以阅读,但很明显实际的 HTTP GET never 被发送所以永远不会调用 /file-stream.mp3 处的端点。记住以下几点更容易理解: 端口 5050 是提供流的服务器。端口 20118 是第一个 some-live-stream 连接的 VLC。端口 20224 是 VLC,与 file-stream.mp3 的第二个连接失败,在底部您可以看到来自端口 20225 的连接正在初始化,这是 some-live-stream 的延续。

出于某种原因,VLC 会立即终止为 /file-stream.mp3 请求新建立的 TCP 连接(查找来自端口 20124->5050 的请求)。所以VLC 主动终止连接:C.

然后在最后几行中重新建立了原始 /some-live-stream.mp3 连接。

那么,为什么 VLC 甚至无法为 /file-stream.mp3 请求发送 HTTP GET?

不要忘记 Play() 并不是您所期望的同步方法。这是一种向后台线程发送停止消息,然后才开始播放媒体的方法。

当您紧接着执行 IsStartingOrPlaying() 方法时,状态可能不是您预期的状态,因此调用第二个 Play()

在@cube45 为我们指明了正确的方向后,手头的问题是一个简单的线程问题,我们所要做的就是在 Enqueue()Stop()Play() 方法如下所示。

public sealed class MediaService
{
    private const int NOT_PLAYING = 0x0;
    private const int PLAYING = 0x1;

    private readonly LibVLC _libVLC;
    private readonly ConcurrentQueue<Uri> _playlist = new();

    private volatile int _playbackStatus = NOT_PLAYING;

    public MediaPlayer VLCPlayer { get; }

    internal MediaService(LibVLC libVLC)
    {
        _libVLC = libVLC;
        VLCPlayer = new MediaPlayer(_libVLC);
        VLCPlayer.EndReached += VLCPlayer_EndReached;
    }
    
    public void Enqueue(Uri uri)
    {
        int status = _playbackStatus;

        if (status is PLAYING)
        {
            _playlist.Enqueue(uri);
            if (Interlocked.CompareExchange(ref _playbackStatus, NOT_PLAYING, PLAYING) is NOT_PLAYING)
            {
                // vlc finished while we were enqueuing...
                DequeueAndPlay();
            }
        }
        else
        {
            Play(uri);
        }
    }

    public bool Play(Uri uri)
    {
        _ = Interlocked.Exchange(ref _playbackStatus, PLAYING);
        Media newMedia = new(_libVLC, uri);
        Media? oldMedia = VLCPlayer.Media;
        bool success = VLCPlayer.Play(newMedia);
        oldMedia?.Dispose();
        CurrentUrl = uri.AbsoluteUri;
        return success;
    }

    public void Stop()
    {
        if (Interlocked.CompareExchange(ref _playbackStatus, NOT_PLAYING, PLAYING) is PLAYING)
        {
            VLCPlayer.Stop();
            if (VLCPlayer.Media is not null)
            {
                Media oldMedia = VLCPlayer.Media;
                VLCPlayer.Media = null;
                oldMedia.Dispose();
            }
        }
    }

    private void VLCPlayer_EndReached(object sender, EventArgs e) => DequeueAndPlay();

    private void DequeueAndPlay()
    {
        if (_playlist.TryDequeue(out Uri? result))
        {
            Task.Run(() => Play(result));
        }
        else
        {
            _ = Interlocked.Exchange(ref _playbackStatus, NOT_PLAYING);
        }
    }
}

虽然上面的代码绝不是“完美”的解决方案,也没有为调用 Enqueue()Stop()Play() 的多个线程提供线程安全,但是它确实减轻了我们 运行 遇到的确切问题,它应该为遇到类似问题的任何人指出正确的方向:)