C# LibVLCSharp切换媒体流导致HTTP异常
C# LibVLCSharp switching media stream causes HTTP exception
我们正在使用 libvlcsharp to play a live mp3 network stream in our xamarin.ios 应用,使用以下代码片段
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 连接的 FIN
、FIN-ACK
、ACK
与 SYN
、SYN-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()
的多个线程提供线程安全,但是它确实减轻了我们 运行 遇到的确切问题,它应该为遇到类似问题的任何人指出正确的方向:)
我们正在使用 libvlcsharp to play a live mp3 network stream in our xamarin.ios 应用,使用以下代码片段
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 连接的 FIN
、FIN-ACK
、ACK
与 SYN
、SYN-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()
的多个线程提供线程安全,但是它确实减轻了我们 运行 遇到的确切问题,它应该为遇到类似问题的任何人指出正确的方向:)