Skip to content

Commit 3a30e10

Browse files
committed
修复HLS对小主播会碎成比较多文件的问题
1 parent 88c9d91 commit 3a30e10

1 file changed

Lines changed: 219 additions & 13 deletions

File tree

  • Core/RuntimeObject/Download

Core/RuntimeObject/Download/HLS.cs

Lines changed: 219 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,11 @@ await Task.Factory.StartNew(() =>
9191
bool InitialRequest = true;
9292
long currentLocation = 0;
9393
long StartLiveTime = card.live_time.Value;
94-
string lastMapUri = string.Empty;
94+
//上一轮init segment的解码配置签名(视频宽高/编码参数、音频采样率/声道/AudioSpecificConfig)。
95+
//注意:不能按Map_URI文件名或init segment逐字节内容比较——部分CDN的m3u8中每个分片都有
96+
//独立的、按秒递增命名的EXT-X-MAP,且init segment里的码率统计等字段会周期性抖动,
97+
//按文件名或全字节比较都会误判为init segment变化,造成无限切割产生大量碎片文件
98+
byte[] lastInitSignature = null;
9599
//最后一次下载到新分片的时间,用于"无新分片"看门狗
96100
DateTime lastSegmentTime = DateTime.Now;
97101

@@ -219,7 +223,6 @@ await Task.Factory.StartNew(() =>
219223
string DebugFile = string.Empty;
220224
//DebugFile = $"{hostClass.eXTM3U.eXTINFs[0].FileName+"_"+hostClass.eXTM3U.Map_URI.Replace(".m4s","_I.m4s")}";
221225
downloadSizeForThisCycle += WriteToFile(fs, $"{hostClass.host}{hostClass.base_url}{hostClass.eXTM3U.Map_URI}?{hostClass.extra}", DebugFile);
222-
lastMapUri = hostClass.eXTM3U.Map_URI;
223226
}
224227
try
225228
{
@@ -248,29 +251,33 @@ await Task.Factory.StartNew(() =>
248251
hlsState = DownloadTaskState.Success;
249252
return;
250253
}
254+
//提取init segment解码配置签名并比较:只有宽高/编码参数/音频参数真正变化(主播连麦/重新推流)才切割
255+
byte[] initSignature = GetInitSegmentSignature(m4sBytes);
256+
if (initSignature != null)
257+
{
258+
if (lastInitSignature != null && !initSignature.SequenceEqual(lastInitSignature))
259+
{
260+
Log.Info(nameof(DlwnloadHls_avc_mp4), $"[{card.Name}({card.RoomId})]检测到推流编码参数变化(可能是主播连麦或重新推流),进行切割处理");
261+
hlsState = DownloadTaskState.Success;
262+
return;
263+
}
264+
lastInitSignature = initSignature;
265+
}
251266
}
252267
else
253268
{
254-
//分辨率检测是可选增强逻辑,init segment 异常时仅跳过本轮检测,不影响录制
269+
//分辨率/init segment内容检测是可选增强逻辑,init segment 异常时仅跳过本轮检测,不影响录制
255270
string skipReason = m4sBytes == null
256271
? "init segment下载失败(未获取到数据,可能为网络问题或CDN拒绝请求)"
257272
: m4sBytes.Length < 248
258273
? $"init segment响应内容过短(仅{m4sBytes.Length}字节,可能为CDN错误页或响应被截断)"
259274
: "init segment内容不是合法的m4s文件(缺少ftyp文件头,可能为CDN错误页)";
260-
Log.Warn(nameof(DlwnloadHls_avc_mp4), $"[{card.Name}({card.RoomId})]本轮分辨率变化检测已跳过{skipReason},录制不受影响,下一轮将自动重试");
275+
Log.Warn(nameof(DlwnloadHls_avc_mp4), $"[{card.Name}({card.RoomId})]本轮分辨率/init segment变化检测已跳过{skipReason},录制不受影响,下一轮将自动重试");
261276
}
262277
}
263278
catch (Exception ex)
264279
{
265-
Log.Warn(nameof(DlwnloadHls_avc_mp4), $"[{card.Name}({card.RoomId})]本轮分辨率变化检测已跳过:下载或解析init segment时发生异常({ex.Message}),录制不受影响", ex);
266-
}
267-
268-
// 检测 init segment 是否变化(主播连麦/重新推流后 B站会发送新的 init segment)
269-
if (!InitialRequest && !string.IsNullOrEmpty(lastMapUri) && hostClass.eXTM3U.Map_URI != lastMapUri)
270-
{
271-
Log.Info(nameof(DlwnloadHls_avc_mp4), $"[{card.Name}({card.RoomId})]检测到 init segment 变化(可能是主播连麦或重新推流),进行切割处理");
272-
hlsState = DownloadTaskState.Success;
273-
return;
280+
Log.Warn(nameof(DlwnloadHls_avc_mp4), $"[{card.Name}({card.RoomId})]本轮分辨率/init segment变化检测已跳过:下载或解析init segment时发生异常({ex.Message}),录制不受影响", ex);
274281
}
275282

276283
if (currentLocation != 0 && Core.Config.Core_RunConfig._ReconnectAnchorReStream)
@@ -407,6 +414,205 @@ private static bool TryParseResolution(byte[] m4sBytes, out long width, out long
407414
return true;
408415
}
409416

417+
/// <summary>
418+
/// 从init segment(m4s)提取解码配置签名:视频宽高+编码配置(avcC/hvcC/av1C)内容、音频声道数/采样率/AudioSpecificConfig。
419+
/// 只有这些影响分片拼接解码的字段变化才视为推流参数真正变化;
420+
/// B站CDN下发的init segment中码率统计等字段会周期性抖动,逐字节比较会误判
421+
/// </summary>
422+
/// <param name="m4sBytes">init segment字节内容</param>
423+
/// <returns>签名字节;内容缺失、过短或不是合法m4s时返回null</returns>
424+
private static byte[] GetInitSegmentSignature(byte[] m4sBytes)
425+
{
426+
if (m4sBytes == null || m4sBytes.Length < 248)
427+
{
428+
return null;
429+
}
430+
if (m4sBytes[4] != 'f' || m4sBytes[5] != 't' || m4sBytes[6] != 'y' || m4sBytes[7] != 'p')
431+
{
432+
return null;
433+
}
434+
using MemoryStream signature = new();
435+
//视频轨道宽高(tkhd固定偏移,与TryParseResolution一致)
436+
signature.Write(m4sBytes, 240, 8);
437+
foreach (var moov in EnumerateChildBoxes(m4sBytes, 0, m4sBytes.Length))
438+
{
439+
if (moov.type != "moov")
440+
{
441+
continue;
442+
}
443+
foreach (var trak in EnumerateChildBoxes(m4sBytes, moov.contentStart, moov.end))
444+
{
445+
if (trak.type != "trak")
446+
{
447+
continue;
448+
}
449+
//逐层下钻到stsd,取第一个sample entry
450+
var mdia = FindChildBox(m4sBytes, trak.contentStart, trak.end, "mdia");
451+
var minf = mdia == null ? null : FindChildBox(m4sBytes, mdia.Value.contentStart, mdia.Value.end, "minf");
452+
var stbl = minf == null ? null : FindChildBox(m4sBytes, minf.Value.contentStart, minf.Value.end, "stbl");
453+
var stsd = stbl == null ? null : FindChildBox(m4sBytes, stbl.Value.contentStart, stbl.Value.end, "stsd");
454+
if (stsd == null)
455+
{
456+
continue;
457+
}
458+
//stsd内容: version/flags(4) + entry_count(4) + sample entry列表
459+
int entryStart = stsd.Value.contentStart + 8;
460+
if (entryStart + 8 > stsd.Value.end)
461+
{
462+
continue;
463+
}
464+
int entrySize = ReadBoxSize(m4sBytes, entryStart);
465+
string entryType = Encoding.ASCII.GetString(m4sBytes, entryStart + 4, 4);
466+
int entryContent = entryStart + 8;
467+
int entryEnd = entryStart + entrySize;
468+
if (entrySize < 8 || entryEnd > stsd.Value.end)
469+
{
470+
continue;
471+
}
472+
if (entryType is "avc1" or "avc2" or "avc3" or "avc4" or "hev1" or "hvc1" or "av01")
473+
{
474+
//VisualSampleEntry固定头部78字节,之后是编码配置等子box
475+
foreach (var child in EnumerateChildBoxes(m4sBytes, entryContent + 78, entryEnd))
476+
{
477+
if (child.type is "avcC" or "hvcC" or "av1C")
478+
{
479+
signature.Write(m4sBytes, child.contentStart, child.end - child.contentStart);
480+
}
481+
}
482+
}
483+
else if (entryType == "mp4a")
484+
{
485+
//AudioSampleEntry固定头部: reserved(6)+data_reference_index(2)+reserved(8)+channelcount(2)+samplesize(2)+pre_defined(2)+reserved(2)+samplerate(4)
486+
if (entryContent + 28 <= entryEnd)
487+
{
488+
signature.Write(m4sBytes, entryContent + 16, 2);//channelcount
489+
signature.Write(m4sBytes, entryContent + 24, 4);//samplerate(16.16定点数)
490+
foreach (var child in EnumerateChildBoxes(m4sBytes, entryContent + 28, entryEnd))
491+
{
492+
if (child.type == "esds")
493+
{
494+
byte[] asc = FindAudioSpecificConfig(m4sBytes, child.contentStart, child.end);
495+
if (asc != null)
496+
{
497+
signature.Write(asc, 0, asc.Length);
498+
}
499+
}
500+
}
501+
}
502+
}
503+
}
504+
}
505+
return signature.ToArray();
506+
}
507+
508+
/// <summary>
509+
/// 在指定范围内查找第一个指定类型的MP4子box
510+
/// </summary>
511+
private static (int contentStart, int end)? FindChildBox(byte[] buf, int start, int end, string type)
512+
{
513+
foreach (var box in EnumerateChildBoxes(buf, start, end))
514+
{
515+
if (box.type == type)
516+
{
517+
return (box.contentStart, box.end);
518+
}
519+
}
520+
return null;
521+
}
522+
523+
/// <summary>
524+
/// 枚举指定范围内的MP4子box
525+
/// </summary>
526+
private static IEnumerable<(int contentStart, int end, string type)> EnumerateChildBoxes(byte[] buf, int start, int end)
527+
{
528+
int i = start;
529+
while (i + 8 <= end)
530+
{
531+
int size = ReadBoxSize(buf, i);
532+
if (size < 8 || i + size > end)
533+
{
534+
yield break;
535+
}
536+
yield return (i + 8, i + size, Encoding.ASCII.GetString(buf, i + 4, 4));
537+
i += size;
538+
}
539+
}
540+
541+
/// <summary>
542+
/// 读取MP4 box的size字段(大端32位)
543+
/// </summary>
544+
private static int ReadBoxSize(byte[] buf, int offset)
545+
{
546+
return (int)((uint)buf[offset] << 24 | (uint)buf[offset + 1] << 16 | (uint)buf[offset + 2] << 8 | buf[offset + 3]);
547+
}
548+
549+
/// <summary>
550+
/// 从esds box内容中解析DecoderSpecificInfo(tag 0x05,即AudioSpecificConfig)。
551+
/// 注意DecoderConfigDescriptor里的maxBitrate/avgBitrate等码率统计字段会随推流抖动,
552+
/// 不能把esds整体直接参与"init segment是否变化"的比较
553+
/// </summary>
554+
private static byte[] FindAudioSpecificConfig(byte[] buf, int start, int end)
555+
{
556+
//esds内容: version/flags(4) + ES_Descriptor(tag 0x03)
557+
foreach (var esDesc in EnumerateDescriptors(buf, start + 4, end))
558+
{
559+
if (esDesc.tag != 0x03 || esDesc.length < 3)
560+
{
561+
continue;
562+
}
563+
//ES_Descriptor: ES_ID(2) + flags(1) + 子描述符
564+
foreach (var decoderConfig in EnumerateDescriptors(buf, esDesc.payloadStart + 3, esDesc.payloadStart + esDesc.length))
565+
{
566+
if (decoderConfig.tag != 0x04 || decoderConfig.length < 13)
567+
{
568+
continue;
569+
}
570+
//DecoderConfigDescriptor: objectType(1)+streamType(1)+bufferSizeDB(3)+maxBitrate(4)+avgBitrate(4) + 子描述符
571+
foreach (var specific in EnumerateDescriptors(buf, decoderConfig.payloadStart + 13, decoderConfig.payloadStart + decoderConfig.length))
572+
{
573+
if (specific.tag == 0x05)
574+
{
575+
byte[] asc = new byte[specific.length];
576+
Array.Copy(buf, specific.payloadStart, asc, 0, specific.length);
577+
return asc;
578+
}
579+
}
580+
}
581+
}
582+
return null;
583+
}
584+
585+
/// <summary>
586+
/// 枚举MPEG-4描述符(tag + 可扩展length + payload)
587+
/// </summary>
588+
private static IEnumerable<(int tag, int payloadStart, int length)> EnumerateDescriptors(byte[] buf, int start, int end)
589+
{
590+
int i = start;
591+
while (i + 2 <= end)
592+
{
593+
int tag = buf[i++];
594+
int length = 0;
595+
int lengthBytes = 0;
596+
bool continues;
597+
do
598+
{
599+
if (i >= end || ++lengthBytes > 4)
600+
{
601+
yield break;
602+
}
603+
byte b = buf[i++];
604+
length = (length << 7) | (b & 0x7F);
605+
continues = (b & 0x80) != 0;
606+
} while (continues);
607+
if (i + length > end)
608+
{
609+
yield break;
610+
}
611+
yield return (tag, i, length);
612+
i += length;
613+
}
614+
}
615+
410616
/// <summary>
411617
/// 处理Host刷新
412618
/// </summary>

0 commit comments

Comments
 (0)