【问题标题】:C# LibVLCSharp switching media stream causes HTTP exceptionC#LibVLCSharp切换媒体流导致HTTP异常
【发布时间】:2021-11-26 22:42:46
【问题描述】:

我们正在使用 在我们的 应用程序中使用以下代码sn-p 播放实时mp3 网络流

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 类的一个实例。上面的代码可以正常工作,并且流媒体可以完美运行。

但是:

在某些时候我们需要切换流(即原始的实时 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

(上面的 sn-p 从第二个 Play() 被调用时开始。)

立即引起注意的是HTTP connection failure

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

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

因此,我们内部 VLC 包装类的整个相关代码实际上如下所示:

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://127.0.0.1:5050/file-stream.mp3 时的 HTTP 异常显然会导致原始问题中描述的“滞后”,然后原始流按预期继续。

但是 此代码 sn-p 有效:

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() 方法似乎是罪魁祸首。但是,当/file-stream.mp3 端点经过良好测试甚至在第二个示例中有效时,我不明白为什么它的实现会成为问题并导致看似随机 HTTP 连接失败。

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

更新 2

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

我们可以看到与第一个/some-live-stream.mp3的终止TCP连接的FINFIN-ACKACK与对@的HTTP请求的SYNSYN-ACKACK混合在一起987654355@ 所以它有点难以阅读,但很明显实际的 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-&gt;5050 的请求)。所以 VLC 主动终止连接 :C.

然后在最后几行中,原来的/some-live-stream.mp3 连接被重新建立。

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

【问题讨论】:

  • 确实应该。请发布一个最小的项目来重现您的问题
  • 也共享 libvlc 详细日志
  • @cube45 我相应地更新了问题并提供了更多上下文,因为该错误似乎不在最初发布的代码中。
  • 当我们要求一个最小的复制时,它在 Main 的行数的意义上不是最小的,但在涉及的复杂性方面是最小的。您将问题定位到 Enqueue 方法,但没有进一步简化。
  • @cube45 在对原始问题进行过度简化之后,我包含了来自我们的MediaService 类的所有相关代码,这些代码将被执行以确保:) 所以是的,你是对的它可以进一步简化,但足以查明确切的问题。感谢您抽出宝贵的时间:)

标签: libvlcsharp xamarin.ios c# .net-core streaming libvlc libvlcsharp


【解决方案1】:

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

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

【讨论】:

  • 你是对的。这似乎确实是一个线程问题。当调试和中断入队方法执行流程时,预期的方式暗示在我们的IsStartingOrPlaying()检查vlcplayer的播放状态更改期间或之后不久,因此执行“不正确”方法(Enqueue()直接调用Play()取消@ 987654324@)。我们将不得不考虑线程安全。感谢您为我们指明正确的方向:)
【解决方案2】:

在@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() 的多个线程提供线程安全,但它确实缓解了我们遇到的确切问题,它应该为遇到类似问题的人指出正确的方向:)

【讨论】:

    猜你喜欢
    • 2012-06-16
    • 1970-01-01
    • 2014-12-10
    • 2011-04-05
    • 2012-06-07
    • 2011-09-29
    • 1970-01-01
    • 2018-05-06
    • 1970-01-01
    相关资源
    最近更新 更多