支持录制画质选择

This commit is contained in:
Genteure 2021-07-09 19:48:39 +08:00
parent f0f45bab39
commit 2b482a9c4b
10 changed files with 150 additions and 21 deletions

View File

@ -110,12 +110,12 @@ namespace BililiveRecorder.Core.Api.Http
return FetchAsync<UserInfo>(this.mainClient, url); return FetchAsync<UserInfo>(this.mainClient, url);
} }
public Task<BilibiliApiResponse<RoomPlayInfo>> GetStreamUrlAsync(int roomid) public Task<BilibiliApiResponse<RoomPlayInfo>> GetStreamUrlAsync(int roomid, int qn)
{ {
if (this.disposedValue) if (this.disposedValue)
throw new ObjectDisposedException(nameof(HttpApiClient)); throw new ObjectDisposedException(nameof(HttpApiClient));
var url = $@"{this.config.LiveApiHost}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={roomid}&protocol=0%2C1&format=0%2C2&codec=0%2C1&qn=10000&platform=web&ptype=16"; var url = $@"{this.config.LiveApiHost}/xlive/web-room/v2/index/getRoomPlayInfo?room_id={roomid}&protocol=0,1&format=0,1,2&codec=0,1&qn={qn}&platform=web&ptype=8";
return FetchAsync<RoomPlayInfo>(this.mainClient, url); return FetchAsync<RoomPlayInfo>(this.mainClient, url);
} }

View File

@ -8,6 +8,6 @@ namespace BililiveRecorder.Core.Api
{ {
Task<BilibiliApiResponse<RoomInfo>> GetRoomInfoAsync(int roomid); Task<BilibiliApiResponse<RoomInfo>> GetRoomInfoAsync(int roomid);
Task<BilibiliApiResponse<UserInfo>> GetUserInfoAsync(int roomid); Task<BilibiliApiResponse<UserInfo>> GetUserInfoAsync(int roomid);
Task<BilibiliApiResponse<RoomPlayInfo>> GetStreamUrlAsync(int roomid); Task<BilibiliApiResponse<RoomPlayInfo>> GetStreamUrlAsync(int roomid, int qn);
} }
} }

View File

@ -0,0 +1,26 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using static BililiveRecorder.Core.Api.Model.RoomPlayInfo;
namespace BililiveRecorder.Core.Api
{
public static class IApiClientExtensions
{
public static async Task<CodecItem?> GetCodecItemInStreamUrlAsync(this IApiClient apiClient, int roomid, int qn)
{
var apiResp = await apiClient.GetStreamUrlAsync(roomid: roomid, qn: qn).ConfigureAwait(false);
var url_data = apiResp?.Data?.PlayurlInfo?.Playurl?.Streams;
if (url_data is null)
throw new Exception("playurl is null");
var url_http_stream_flv_avc =
url_data.FirstOrDefault(x => x.ProtocolName == "http_stream")
?.Formats?.FirstOrDefault(x => x.FormatName == "flv")
?.Codecs?.FirstOrDefault(x => x.CodecName == "avc");
return url_http_stream_flv_avc;
}
}
}

View File

@ -27,9 +27,9 @@ namespace BililiveRecorder.Core.Api
.ExecuteAsync(_ => this.client.GetRoomInfoAsync(roomid), new Context(PolicyNames.CacheKeyRoomInfo + ":" + roomid)) .ExecuteAsync(_ => this.client.GetRoomInfoAsync(roomid), new Context(PolicyNames.CacheKeyRoomInfo + ":" + roomid))
.ConfigureAwait(false); .ConfigureAwait(false);
public async Task<BilibiliApiResponse<RoomPlayInfo>> GetStreamUrlAsync(int roomid) => await this.policies public async Task<BilibiliApiResponse<RoomPlayInfo>> GetStreamUrlAsync(int roomid, int qn) => await this.policies
.Get<IAsyncPolicy>(PolicyNames.PolicyStreamApiRequestAsync) .Get<IAsyncPolicy>(PolicyNames.PolicyStreamApiRequestAsync)
.ExecuteAsync(_ => this.client.GetStreamUrlAsync(roomid), new Context(PolicyNames.CacheKeyStream + ":" + roomid)) .ExecuteAsync(_ => this.client.GetStreamUrlAsync(roomid, qn), new Context(PolicyNames.CacheKeyStream + ":" + roomid + ":" + qn))
.ConfigureAwait(false); .ConfigureAwait(false);
public async Task<BilibiliApiResponse<UserInfo>> GetUserInfoAsync(int roomid) => await this.policies public async Task<BilibiliApiResponse<UserInfo>> GetUserInfoAsync(int roomid) => await this.policies

View File

@ -92,6 +92,14 @@ namespace BililiveRecorder.Core.Config.V2
[JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)] [JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); } public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); }
/// <summary>
/// 录制的直播画质 qn 值,逗号分割,靠前的优先
/// </summary>
public string RecordingQuality { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasRecordingQuality { get => this.GetPropertyHasValue(nameof(this.RecordingQuality)); set => this.SetPropertyHasValue<string>(value, nameof(this.RecordingQuality)); }
[JsonProperty(nameof(RecordingQuality)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string> OptionalRecordingQuality { get => this.GetPropertyValueOptional<string>(nameof(this.RecordingQuality)); set => this.SetPropertyValueOptional(value, nameof(this.RecordingQuality)); }
/// <summary> /// <summary>
/// 录制断开重连时间间隔 毫秒 /// 录制断开重连时间间隔 毫秒
/// </summary> /// </summary>
@ -317,6 +325,14 @@ namespace BililiveRecorder.Core.Config.V2
[JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)] [JsonProperty(nameof(RecordDanmakuGuard)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); } public Optional<bool> OptionalRecordDanmakuGuard { get => this.GetPropertyValueOptional<bool>(nameof(this.RecordDanmakuGuard)); set => this.SetPropertyValueOptional(value, nameof(this.RecordDanmakuGuard)); }
/// <summary>
/// 录制的直播画质 qn 值,逗号分割,靠前的优先
/// </summary>
public string RecordingQuality { get => this.GetPropertyValue<string>(); set => this.SetPropertyValue(value); }
public bool HasRecordingQuality { get => this.GetPropertyHasValue(nameof(this.RecordingQuality)); set => this.SetPropertyHasValue<string>(value, nameof(this.RecordingQuality)); }
[JsonProperty(nameof(RecordingQuality)), EditorBrowsable(EditorBrowsableState.Never)]
public Optional<string> OptionalRecordingQuality { get => this.GetPropertyValueOptional<string>(nameof(this.RecordingQuality)); set => this.SetPropertyValueOptional(value, nameof(this.RecordingQuality)); }
} }
public sealed partial class DefaultConfig public sealed partial class DefaultConfig
@ -364,6 +380,8 @@ namespace BililiveRecorder.Core.Config.V2
public bool RecordDanmakuGuard => true; public bool RecordDanmakuGuard => true;
public string RecordingQuality => "10000";
} }
} }

View File

@ -64,7 +64,7 @@ module.exports = {
"type": "bool", "type": "bool",
"desc": "是否显示直播间标题和分区", "desc": "是否显示直播间标题和分区",
"default": "true", "default": "true",
}, ], },],
"room": [{ "room": [{
"name": "RoomId", "name": "RoomId",
"type": "int", "type": "int",
@ -117,5 +117,10 @@ module.exports = {
"type": "bool", "type": "bool",
"desc": "是否同时录制 上船", "desc": "是否同时录制 上船",
"default": "true" "default": "true"
}, ] }, {
"name": "RecordingQuality",
"type": "string",
"desc": "录制的直播画质 qn 值,逗号分割,靠前的优先",
"default": "\"10000\""
},]
} }

View File

@ -139,6 +139,21 @@
} }
} }
}, },
"RecordingQuality": {
"description": "录制的直播画质 qn 值,逗号分割,靠前的优先",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": "10000"
}
}
},
"TimingStreamRetry": { "TimingStreamRetry": {
"description": "录制断开重连时间间隔 毫秒", "description": "录制断开重连时间间隔 毫秒",
"type": "object", "type": "object",
@ -471,6 +486,21 @@
} }
} }
}, },
"RecordingQuality": {
"description": "录制的直播画质 qn 值,逗号分割,靠前的优先",
"type": "object",
"additionalProperties": false,
"properties": {
"HasValue": {
"type": "boolean",
"default": true
},
"Value": {
"type": "string",
"default": "10000"
}
}
},
"RoomId": { "RoomId": {
"description": "房间号", "description": "房间号",
"type": "object", "type": "object",

View File

@ -236,24 +236,56 @@ namespace BililiveRecorder.Core.Recording
protected async Task<string> FetchStreamUrlAsync(int roomid) protected async Task<string> FetchStreamUrlAsync(int roomid)
{ {
var apiResp = await this.apiClient.GetStreamUrlAsync(roomid: roomid).ConfigureAwait(false); const int DefaultQn = 10000;
var url_data = apiResp?.Data?.PlayurlInfo?.Playurl?.Streams; var selected_qn = DefaultQn;
int[] qns;
Api.Model.RoomPlayInfo.UrlInfoItem[]? url_infos;
if (url_data is null) var codecItem = await this.apiClient.GetCodecItemInStreamUrlAsync(roomid: roomid, qn: DefaultQn).ConfigureAwait(false);
throw new Exception("playurl is null");
var url_http_stream_flv_avc = if (codecItem is null)
url_data.FirstOrDefault(x => x.ProtocolName == "http_stream") throw new Exception("no supported stream url, qn: " + DefaultQn);
?.Formats?.FirstOrDefault(x => x.FormatName == "flv")
?.Codecs?.FirstOrDefault(x => x.CodecName == "avc");
if (url_http_stream_flv_avc is null) {
throw new Exception("no supported stream url"); try
{
qns = this.room.RoomConfig.RecordingQuality.Split(new[] { ',', '', '、', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(x => int.TryParse(x, out var num) ? num : -1)
.Where(x => x > 0)
.ToArray();
if (url_http_stream_flv_avc.CurrentQn != 10000) foreach (var qn in qns)
this.logger.Warning("当前录制的画质是 {CurrentQn}", url_http_stream_flv_avc.CurrentQn); {
if (codecItem.AcceptQn.Contains(qn))
{
selected_qn = qn;
break;
}
}
var url_infos = url_http_stream_flv_avc.UrlInfos; this.logger.Debug("设置画质 {QnSettings}, 可用画质 {AcceptQn}, 最终选择 {SelectedQn}", qns, codecItem.AcceptQn, selected_qn);
}
catch (Exception ex)
{
this.logger.Warning(ex, "判断录制画质时出错,将默认使用 原画(10000)");
qns = new[] { DefaultQn };
url_infos = codecItem.UrlInfos;
}
}
if (selected_qn != DefaultQn)
{
// 最终选择的 qn 与默认不同,需要重新请求一次
codecItem = await this.apiClient.GetCodecItemInStreamUrlAsync(roomid: roomid, qn: selected_qn).ConfigureAwait(false);
if (codecItem is null)
throw new Exception("no supported stream url, qn: " + DefaultQn);
}
if (codecItem.CurrentQn != selected_qn || !qns.Contains(codecItem.CurrentQn))
this.logger.Warning("当前录制的画质是 {CurrentQn}", codecItem.CurrentQn);
url_infos = codecItem.UrlInfos;
if (url_infos is null || url_infos.Length == 0) if (url_infos is null || url_infos.Length == 0)
throw new Exception("no url_info"); throw new Exception("no url_info");
@ -264,7 +296,7 @@ namespace BililiveRecorder.Core.Recording
? url_infos_without_mcdn[this.random.Next(url_infos_without_mcdn.Length)] ? url_infos_without_mcdn[this.random.Next(url_infos_without_mcdn.Length)]
: url_infos[this.random.Next(url_infos.Length)]; : url_infos[this.random.Next(url_infos.Length)];
var fullUrl = url_info.Host + url_http_stream_flv_avc.BaseUrl + url_info.Extra; var fullUrl = url_info.Host + codecItem.BaseUrl + url_info.Extra;
return fullUrl; return fullUrl;
} }

View File

@ -88,6 +88,15 @@
</local:SettingWithDefault> </local:SettingWithDefault>
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<GroupBox Header="录制画质">
<StackPanel>
<TextBlock Text="逗号分割的录制画质ID"/>
<local:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordingQuality}">
<TextBox Text="{Binding RecordingQuality,UpdateSourceTrigger=PropertyChanged,Delay=1000}"
ui:TextBoxHelper.IsDeleteButtonVisible="False" Width="220" HorizontalAlignment="Left"/>
</local:SettingWithDefault>
</StackPanel>
</GroupBox>
</ui:SimpleStackPanel> </ui:SimpleStackPanel>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>

View File

@ -99,6 +99,15 @@
Text="{Binding WebHookUrls,UpdateSourceTrigger=PropertyChanged,Delay=1000}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/> Text="{Binding WebHookUrls,UpdateSourceTrigger=PropertyChanged,Delay=1000}" ui:TextBoxHelper.IsDeleteButtonVisible="False"/>
</StackPanel> </StackPanel>
</GroupBox> </GroupBox>
<GroupBox Header="录制画质">
<StackPanel MaxWidth="400" HorizontalAlignment="Left">
<TextBlock Text="逗号分割的录制画质ID"/>
<c:SettingWithDefault IsSettingNotUsingDefault="{Binding HasRecordingQuality}">
<TextBox Text="{Binding RecordingQuality,UpdateSourceTrigger=PropertyChanged,Delay=1000}"
ui:TextBoxHelper.IsDeleteButtonVisible="False" Width="220" HorizontalAlignment="Left"/>
</c:SettingWithDefault>
</StackPanel>
</GroupBox>
</ui:SimpleStackPanel> </ui:SimpleStackPanel>
</ScrollViewer> </ScrollViewer>
</ui:Page> </ui:Page>