C# 使用LibVLCSharp切换媒体流触发HTTP异常问题排查
我们在Xamarin.iOS应用中使用libvlcsharp播放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,请问是什么原因?该如何修复?
更新(疑似集成bug)
开启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报错。
补充最小复现场景:生产环境中我们需要多次切换流,首先拉取服务端24小时不间断运行的some-live-stream.mp3直播流,之后需要切换到时长约10秒的文件流file-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异常就是原问题中描述的“卡顿”的诱因,异常抛出后原直播流会继续正常播放。
但是以下代码片段可以正常运行:
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连接的FIN、FIN-ACK、ACK包,与请求/file-stream.mp3的SYN、SYN-ACK、ACK包混杂在一起,可读性较差,但可以确认HTTP GET请求实际上从未发送,所以/file-stream.mp3接口根本没有被调用。参考信息如下:
端口5050是提供流媒体的服务端端口,端口20118是VLC建立的第一个some-live-stream连接的端口,端口20224是VLC建立的第二个请求file-stream.mp3的连接端口(该连接失败),最底部可以看到端口20225发起的连接初始化请求,是后续重新拉取some-live-stream的连接。
VLC在/file-stream.mp3请求的TCP连接刚建立完成时就主动终止了该连接(可查看端口20124->5050的请求),即VLC主动断开了连接。最后几行日志显示原/some-live-stream.mp3连接被重新建立。请问为什么VLC甚至没有发送/file-stream.mp3的HTTP GET请求就终止了连接?
根本原因
问题核心出在Enqueue方法的判断逻辑和EndReached回调的时序冲突:
- 调用
Play切到file-stream.mp3后,VLC的状态更新不是同步的,此时立刻调用Enqueue方法,IsStartingOrPlaying()判断的仍然是上一个直播流的播放状态,导致要切回去的直播流被直接塞进了队列。 file-stream.mp3刚完成初始化、甚至还没来得及发GET请求,EndReached回调就被触发(本质是前一个流的状态更新延迟+新流加载初期的状态判断误差,加上调用Enqueue的时机过早,队列里已经有了要切回的直播流),于是立刻执行Play(result)把直播流又切了回去,VLC直接终止了还在初始化阶段的file-stream.mp3的TCP连接,就出现了抓包看到的刚建连就断开、没有发GET请求的现象。- 加了
Thread.Sleep(10000)的示例正常,是因为等到file-stream.mp3已经完整加载播放、状态完全更新后,才调用Play切回直播流,不存在队列触发的提前切换。
修复方案
1. 调整Enqueue逻辑,加入线程安全的状态锁和状态校验
首先修复IsStartingOrPlaying的判断时机,同时在播放切换时标记正在切换状态,避免误判:
private readonly object _stateLock = new(); private bool _isSwitching = false; public bool IsStartingOrPlaying() { lock (_stateLock) { if (_isSwitching) return false; return VLCPlayer.State is VLCState.Buffering or VLCState.Opening or VLCState.Playing; } }
2. 修改Play方法,标记切换状态,等待旧资源完全释放
public bool Play(Uri uri) { lock (_stateLock) { _isSwitching = true; // 先主动停止旧播放,不要等Play方法自动替换,确保旧连接完全释放 if (VLCPlayer.IsPlaying) { VLCPlayer.Stop(); } Media newMedia = new(_libVLC, uri); Media? oldMedia = VLCPlayer.Media; bool success = VLCPlayer.Play(newMedia); oldMedia?.Dispose(); CurrentUrl = uri.AbsoluteUri; _isSwitching = false; return success; } }
3. 修复EndReached回调的逻辑,过滤非当前流的回调
LibVLC的EndReached回调存在时序问题,可能收到之前已经被替换掉的Media的回调,需要加校验,只有当前流播放结束才处理队列:
private string CurrentUrl { get; set; } private void VLCPlayer_EndReached(object sender, EventArgs e) { Uri? nextUri = null; lock (_stateLock) { if (_playlist.TryDequeue(out var result) && !_isSwitching) { nextUri = result; } } if (nextUri != null) { Task.Run(() => Play(nextUri)); } }
4. 额外优化
如果需要更高的可靠性,可以在创建Media后先调用Parse方法校验资源可用性,再调用Play,避免无效的TCP连接请求。
内容的提问来源于stack exchange,提问作者Frederik Hoeft

