From 550d858d8df7bc3745be1068a2b909c2b6658aca Mon Sep 17 00:00:00 2001 From: Misaka-L Date: Sun, 28 Jul 2024 18:15:54 +0800 Subject: [PATCH] feat: get dash video url --- .../Controllers/BiliVideoController.cs | 127 ++++++++++++++- .../ApiResponse/MisakaStreamUrlResponse.cs | 25 +-- .../Models/DashRequestRedirectType.cs | 8 + .../Models/BiliApi/BiliVideoQuality.cs | 26 ++- .../Models/BiliApi/BiliVideoUrlResponse.cs | 102 ------------ .../BiliApi/BiliVideoUrlResponseBase.cs | 153 ++++++++++++++++++ .../Services/BiliApi/IBiliApiServices.cs | 22 ++- MisakaBiliCore/Utils/NoP2PUtils.cs | 11 +- 8 files changed, 346 insertions(+), 128 deletions(-) create mode 100644 MisakaBiliApi/Models/DashRequestRedirectType.cs delete mode 100644 MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponse.cs create mode 100644 MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponseBase.cs diff --git a/MisakaBiliApi/Controllers/BiliVideoController.cs b/MisakaBiliApi/Controllers/BiliVideoController.cs index b02ba92..b405f78 100644 --- a/MisakaBiliApi/Controllers/BiliVideoController.cs +++ b/MisakaBiliApi/Controllers/BiliVideoController.cs @@ -1,4 +1,5 @@ using Microsoft.AspNetCore.Mvc; +using MisakaBiliApi.Models; using MisakaBiliApi.Models.ApiResponse; using MisakaBiliCore.Models.BiliApi; using MisakaBiliCore.Services.BiliApi; @@ -48,9 +49,10 @@ public class BiliVideoController(IBiliApiServices biliApiServices) : ControllerB /// 重定向到视频流地址 [HttpGet("url/mp4")] [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status302Found)] [Produces("application/json")] - public async ValueTask> GetVideoUrl(string bvid = "", + public async ValueTask> GetVideoMp4Url(string bvid = "", string avid = "", int page = 0, bool redirect = false) { if (string.IsNullOrWhiteSpace(bvid) && string.IsNullOrWhiteSpace(avid)) @@ -74,10 +76,9 @@ public async ValueTask> GetVideoUrl(s var pageDetail = await GetVideoPageInternal(bvid, avid, page); var urlResponse = useBvid - ? await biliApiServices.GetVideoUrlByBvid(bvid, pageDetail.Cid, (int)BiliVideoQuality.R1080PHighRate, - (int)BiliVideoStreamType.Mp4) - : await biliApiServices.GetVideoUrlByAvid(avid, pageDetail.Cid, (int)BiliVideoQuality.R1080PHighRate, - (int)BiliVideoStreamType.Mp4); + ? await biliApiServices.GetVideoMp4UrlByBvid(bvid, pageDetail.Cid, (int)BiliVideoQuality.R1080PHighRate) + : await biliApiServices.GetVideoMp4UrlByAvid(avid, pageDetail.Cid, + (int)BiliVideoQuality.R1080PHighRate); var urls = urlResponse.Data.Durl.SelectMany(durl => (string[]) [durl.Url, ..durl.BackupUrl ?? []]) .ToArray(); @@ -88,7 +89,7 @@ public async ValueTask> GetVideoUrl(s return Redirect(url); } - return new MisakaVideoStreamUrlResponse( + return new MisakaVideoStreamMp4UrlResponse( Url: url, Format: urlResponse.Data.Format, TimeLength: urlResponse.Data.Timelength, @@ -102,6 +103,115 @@ public async ValueTask> GetVideoUrl(s } } + /// + /// 请求哔哩哔哩视频流地址 (DASH) + /// + /// BV 号 + /// AV 号(纯数字) + /// 分 P(从 0 开始) + /// 重定向到视频流或音频流 URL或不重定向 + /// 返回或重定向到 MP4 视频流地址 + /// + /// 示例请求(BV 号): + /// + /// GET /video/url/mp4?bvid=BV1LP411v7Bv + /// GET /video/url/mp4?bvid=BV1LP411v7Bv&redirect=true (获取视频流 URL 并重定向) + /// GET /video/url/mp4?bvid=BV1mx411M793&page=2 (获取 P3 的视频流 URL) + /// + /// 示例请求(AV 号): + /// + /// GET /video/url/mp4?avid=315594987 + /// GET /video/url/mp4?avid=315594987&redirect=true (获取视频流 URL 并重定向) + /// GET /video/url/mp4?bvid=15627712&page=2 (获取 P3 的视频流 URL) + /// + /// 示例响应: + /// + /// { + /// "url": "https://*.bilivideo.com/*", + /// "format": "mp4720", + /// "timeLength": 222000, + /// "quality": 64 + /// } + /// + /// 请求参数错误 + /// 返回视频流地址 + [HttpGet("url/dash")] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + [ProducesResponseType(StatusCodes.Status200OK)] + [Produces("application/json")] + public async ValueTask> GetVideoDashUrl(string bvid = "", + string avid = "", int page = 0, DashRequestRedirectType redirect = DashRequestRedirectType.None) + { + if (string.IsNullOrWhiteSpace(bvid) && string.IsNullOrWhiteSpace(avid)) + { + ModelState.AddModelError(nameof(bvid), "You need at last a bvid or avid"); + ModelState.AddModelError(nameof(avid), "You need at last a bvid or avid"); + } + + if (!string.IsNullOrWhiteSpace(bvid) && !string.IsNullOrWhiteSpace(avid)) + { + ModelState.AddModelError(nameof(bvid), "You input avid and bvid at the sametime"); + ModelState.AddModelError(nameof(avid), "You input avid and bvid at the sametime"); + } + + if (!ModelState.IsValid) return BadRequest(); + + var useBvid = !string.IsNullOrWhiteSpace(bvid); + + try + { + var pageDetail = await GetVideoPageInternal(bvid, avid, page); + + var urlResponse = useBvid + ? await biliApiServices.GetVideoDashUrlByBvid(bvid, pageDetail.Cid, + (int)BiliVideoQuality.R1080PHighRate) + : await biliApiServices.GetVideoDashUrlByAvid(avid, pageDetail.Cid, + (int)BiliVideoQuality.R1080PHighRate); + + var videoDashs = GetDashs(urlResponse.Data.Dash.Video); + if (redirect == DashRequestRedirectType.Video) + { + return Redirect(videoDashs[0].Urls[0]); + } + + var audioDashs = GetDashs(urlResponse.Data.Dash.Audio); + if (redirect == DashRequestRedirectType.Audio) + { + return Redirect(audioDashs[0].Urls[0]); + } + + return new MisakaVideoStreamDashUrlResponse( + VideoDashs: videoDashs, + AudioDashs: audioDashs, + Duration: urlResponse.Data.Dash.Duration + ); + } + catch (ArgumentException argumentException) + { + ModelState.AddModelError(argumentException.ParamName ?? "", argumentException.Message); + return BadRequest(); + } + } + + private static MisakaVideoDashItem[] GetDashs(BiliVideoDashUrlItem[] dashs) + { + return dashs.Select(dash => + { + return new MisakaVideoDashItem( + Urls: NoP2PUtils.GetNoP2PUrls( + [dash.BaseUrl.ToString(), ..dash.BackupUrls?.Select(url => url.ToString()) ?? []]), + FrameRate: dash.FrameRate, + Width: dash.Width, + Height: dash.Height, + Codecs: dash.Codecs, + Bandwidth: dash.Bandwidth, + Id: dash.Id + ); + }) + .OrderByDescending(dash => dash.Bandwidth) + .ToArray(); + } + private async ValueTask GetVideoPageInternal(string? bvid = null, string? avid = null, int page = 0) { if (string.IsNullOrWhiteSpace(bvid) && string.IsNullOrWhiteSpace(avid)) @@ -115,7 +225,8 @@ private async ValueTask GetVideoPageInternal(string? bvid = null, ? (await biliApiServices.GetVideoDetailByBvid(bvid)).Data : (await biliApiServices.GetVideoDetailByAid(avid)).Data; - if (page > videoDetail.Pages.Length - 1) throw new ArgumentOutOfRangeException(nameof(page), "Page out of index"); + if (page > videoDetail.Pages.Length - 1) + throw new ArgumentOutOfRangeException(nameof(page), "Page out of index"); return videoDetail.Pages[page]; } diff --git a/MisakaBiliApi/Models/ApiResponse/MisakaStreamUrlResponse.cs b/MisakaBiliApi/Models/ApiResponse/MisakaStreamUrlResponse.cs index 36805cf..7bfd791 100644 --- a/MisakaBiliApi/Models/ApiResponse/MisakaStreamUrlResponse.cs +++ b/MisakaBiliApi/Models/ApiResponse/MisakaStreamUrlResponse.cs @@ -4,12 +4,6 @@ namespace MisakaBiliApi.Models.ApiResponse; -/// -/// 视频流地址响应数据 -/// -/// 流地址 -public record MisakaStreamUrl(string Url); - /// /// 视频流地址响应数据 /// @@ -17,12 +11,24 @@ public record MisakaStreamUrl(string Url); /// 视频格式,如 mp4720 /// 时长,单位为毫秒 /// 视频流画质 -public record MisakaVideoStreamUrlResponse( +public record MisakaVideoStreamMp4UrlResponse( string Url, string Format, long TimeLength, [property: JsonPropertyName("quality")] - BiliVideoQuality Quality) : MisakaStreamUrl(Url); + BiliVideoQuality Quality); + +public record MisakaVideoStreamDashUrlResponse(MisakaVideoDashItem[] VideoDashs, MisakaVideoDashItem[] AudioDashs, long Duration); + +public record MisakaVideoDashItem( + string[] Urls, + string FrameRate, + long Width, + long Height, + string Codecs, + long Bandwidth, + long Id + ); /// /// 直播流地址响应数据 @@ -30,5 +36,4 @@ public record MisakaVideoStreamUrlResponse( /// 直播流地址 /// 直播流画质 /// 直播流类型 -public record MisakaLiveStreamUrlResponse(string Url, BiliLiveQuality Quality, LiveStreamType StreamType) - : MisakaStreamUrl(Url); +public record MisakaLiveStreamUrlResponse(string Url, BiliLiveQuality Quality, LiveStreamType StreamType); diff --git a/MisakaBiliApi/Models/DashRequestRedirectType.cs b/MisakaBiliApi/Models/DashRequestRedirectType.cs new file mode 100644 index 0000000..3132e11 --- /dev/null +++ b/MisakaBiliApi/Models/DashRequestRedirectType.cs @@ -0,0 +1,8 @@ +namespace MisakaBiliApi.Models; + +public enum DashRequestRedirectType +{ + Video, + Audio, + None +} diff --git a/MisakaBiliCore/Models/BiliApi/BiliVideoQuality.cs b/MisakaBiliCore/Models/BiliApi/BiliVideoQuality.cs index a49c60a..a2a3232 100644 --- a/MisakaBiliCore/Models/BiliApi/BiliVideoQuality.cs +++ b/MisakaBiliCore/Models/BiliApi/BiliVideoQuality.cs @@ -54,7 +54,31 @@ public enum BiliVideoStreamType /// Mp4 = 1, /// + /// DASH + /// + Dash = 16, + /// + /// HDR + /// + HDR = 64, + /// /// 4K /// - R4K = 128 + R4K = 128, + /// + /// Dobly + /// + Dobly = 256, + /// + /// DoblyVision + /// + DoblyVision = 256, + /// + /// 8K + /// + R8K = 1024, + /// + /// AV1 Encoder + /// + AV1 = 2048, } diff --git a/MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponse.cs b/MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponse.cs deleted file mode 100644 index 9bb57f4..0000000 --- a/MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponse.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System.Text.Json.Serialization; - -namespace MisakaBiliCore.Models.BiliApi; - - public record BiliVideoUrlResponse - { - [JsonPropertyName("from")] - public string From { get; init; } - - [JsonPropertyName("result")] - public string Result { get; init; } - - [JsonPropertyName("message")] - public string Message { get; init; } - - [JsonPropertyName("quality")] - public BiliVideoQuality Quality { get; init; } - - [JsonPropertyName("format")] - public string Format { get; init; } - - [JsonPropertyName("timelength")] - public long Timelength { get; init; } - - [JsonPropertyName("accept_format")] - public string AcceptFormat { get; init; } - - [JsonPropertyName("accept_description")] - public string[] AcceptDescription { get; init; } - - [JsonPropertyName("accept_quality")] - public long[] AcceptQuality { get; init; } - - [JsonPropertyName("video_codecid")] - public long VideoCodecid { get; init; } - - [JsonPropertyName("seek_param")] - public string SeekParam { get; init; } - - [JsonPropertyName("seek_type")] - public string SeekType { get; init; } - - [JsonPropertyName("durl")] - public BiliVideoUrlItem[] Durl { get; init; } - - [JsonPropertyName("support_formats")] - public BiliVideoSupportFormat[] SupportFormats { get; init; } - - [JsonPropertyName("high_format")] - public object HighFormat { get; init; } - - [JsonPropertyName("last_play_time")] - public long LastPlayTime { get; init; } - - [JsonPropertyName("last_play_cid")] - public long LastPlayCid { get; init; } - } - - public record BiliVideoUrlItem - { - [JsonPropertyName("order")] - public long Order { get; init; } - - [JsonPropertyName("length")] - public long Length { get; init; } - - [JsonPropertyName("size")] - public long Size { get; init; } - - [JsonPropertyName("ahead")] - public string Ahead { get; init; } - - [JsonPropertyName("vhead")] - public string Vhead { get; init; } - - [JsonPropertyName("url")] - public string Url { get; init; } - - [JsonPropertyName("backup_url")] - public string[]? BackupUrl { get; init; } - } - - public record BiliVideoSupportFormat - { - [JsonPropertyName("quality")] - public long Quality { get; init; } - - [JsonPropertyName("format")] - public string Format { get; init; } - - [JsonPropertyName("new_description")] - public string NewDescription { get; init; } - - [JsonPropertyName("display_desc")] - public string DisplayDesc { get; init; } - - [JsonPropertyName("superscript")] - public string Superscript { get; init; } - - [JsonPropertyName("codecs")] - public object Codecs { get; init; } - } diff --git a/MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponseBase.cs b/MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponseBase.cs new file mode 100644 index 0000000..d670228 --- /dev/null +++ b/MisakaBiliCore/Models/BiliApi/BiliVideoUrlResponseBase.cs @@ -0,0 +1,153 @@ +using System.Text.Json.Serialization; + +namespace MisakaBiliCore.Models.BiliApi; + +#region Common + +public record BiliVideoUrlResponseBase +{ + [JsonPropertyName("from")] public string From { get; init; } + + [JsonPropertyName("result")] public string Result { get; init; } + + [JsonPropertyName("message")] public string Message { get; init; } + + [JsonPropertyName("quality")] public BiliVideoQuality Quality { get; init; } + + [JsonPropertyName("format")] public string Format { get; init; } + + [JsonPropertyName("timelength")] public long Timelength { get; init; } + + [JsonPropertyName("accept_format")] public string AcceptFormat { get; init; } + + [JsonPropertyName("accept_description")] + public string[] AcceptDescription { get; init; } + + [JsonPropertyName("accept_quality")] public long[] AcceptQuality { get; init; } + + [JsonPropertyName("video_codecid")] public long VideoCodecid { get; init; } + + [JsonPropertyName("seek_param")] public string SeekParam { get; init; } + + [JsonPropertyName("seek_type")] public string SeekType { get; init; } + + [JsonPropertyName("support_formats")] public BiliVideoSupportFormat[] SupportFormats { get; init; } + + [JsonPropertyName("high_format")] public object HighFormat { get; init; } + + [JsonPropertyName("last_play_time")] public long LastPlayTime { get; init; } + + [JsonPropertyName("last_play_cid")] public long LastPlayCid { get; init; } +} + +public record BiliVideoSupportFormat +{ + [JsonPropertyName("quality")] public long Quality { get; init; } + + [JsonPropertyName("format")] public string Format { get; init; } + + [JsonPropertyName("new_description")] public string NewDescription { get; init; } + + [JsonPropertyName("display_desc")] public string DisplayDesc { get; init; } + + [JsonPropertyName("superscript")] public string Superscript { get; init; } + + [JsonPropertyName("codecs")] public object Codecs { get; init; } +} + +#endregion + +#region Mp4 + +public record BiliVideoMp4UrlResponse : BiliVideoUrlResponseBase +{ + [JsonPropertyName("durl")] public BiliVideoMp4UrlItem[] Durl { get; init; } = []; +} + +public record BiliVideoMp4UrlItem +{ + [JsonPropertyName("order")] public long Order { get; init; } + + [JsonPropertyName("length")] public long Length { get; init; } + + [JsonPropertyName("size")] public long Size { get; init; } + + [JsonPropertyName("ahead")] public string Ahead { get; init; } + + [JsonPropertyName("vhead")] public string Vhead { get; init; } + + [JsonPropertyName("url")] public string Url { get; init; } + + [JsonPropertyName("backup_url")] public string[]? BackupUrl { get; init; } +} + +#endregion + +#region Dash + +public record BiliVideoDashUrlResponse : BiliVideoUrlResponseBase +{ + [JsonPropertyName("dash")] public BiliVideoDashInfo Dash { get; init; } +} + +public record BiliVideoDashInfo +{ + [JsonPropertyName("duration")] public long Duration { get; set; } + + [JsonPropertyName("minBufferTime")] public double MinBufferTime { get; set; } + + [JsonPropertyName("min_buffer_time")] public double TemperaturesMinBufferTime { get; set; } + + [JsonPropertyName("video")] public BiliVideoDashUrlItem[] Video { get; set; } + + [JsonPropertyName("audio")] public BiliVideoDashUrlItem[] Audio { get; set; } + + [JsonPropertyName("dolby")] public BiliVideoDashDolbyInfo BiliVideoDashDolbyInfo { get; set; } + + [JsonPropertyName("flac")] public object Flac { get; set; } +} + +public record BiliVideoDashUrlItem +{ + [JsonPropertyName("id")] public long Id { get; set; } + + [JsonPropertyName("base_url")] public Uri BaseUrl { get; set; } + + [JsonPropertyName("backup_url")] public Uri[]? BackupUrls { get; set; } + + [JsonPropertyName("bandwidth")] public long Bandwidth { get; set; } + + [JsonPropertyName("mime_type")] public string MimeType { get; set; } + + [JsonPropertyName("codecs")] public string Codecs { get; set; } + + [JsonPropertyName("width")] public long Width { get; set; } + + [JsonPropertyName("height")] public long Height { get; set; } + + [JsonPropertyName("frame_rate")] public string FrameRate { get; set; } + + [JsonPropertyName("sar")] public string Sar { get; set; } + + [JsonPropertyName("startWithSap")] public long StartWithSap { get; set; } + + [JsonPropertyName("start_with_sap")] public long AudioStartWithSap { get; set; } + + [JsonPropertyName("segment_base")] public BiliVideoDashItemSegmentBase egmentBase { get; set; } + + [JsonPropertyName("codecid")] public long Codecid { get; set; } +} + +public record BiliVideoDashItemSegmentBase +{ + [JsonPropertyName("initialization")] public string Initialization { get; set; } + [JsonPropertyName("index_range")] public string IndexRange { get; set; } +} + +public record BiliVideoDashDolbyInfo +{ + [JsonPropertyName("type")] public long Type { get; set; } + [JsonPropertyName("audio")] public object Audio { get; set; } +} + +#endregion diff --git a/MisakaBiliCore/Services/BiliApi/IBiliApiServices.cs b/MisakaBiliCore/Services/BiliApi/IBiliApiServices.cs index d9d7081..162f800 100644 --- a/MisakaBiliCore/Services/BiliApi/IBiliApiServices.cs +++ b/MisakaBiliCore/Services/BiliApi/IBiliApiServices.cs @@ -6,14 +6,28 @@ namespace MisakaBiliCore.Services.BiliApi; public interface IBiliApiServices { [Get("/x/player/wbi/playurl")] - public Task> GetVideoUrlByBvid(string bvid, long cid, - [AliasAs("qn")] int quality, [AliasAs("fnval")] int streamType, + public Task> GetVideoMp4UrlByBvid(string bvid, long cid, + [AliasAs("qn")] int quality, [AliasAs("fnval")] int streamType = (int)BiliVideoStreamType.Mp4, [AliasAs("fourk")] int allow4K = 1, [AliasAs("fnver")] int streamTypeVersion = 0, string platform = "html5", [AliasAs("high_quality")] int highQuality = 1); [Get("/x/player/wbi/playurl")] - public Task> GetVideoUrlByAvid(string avid, long cid, - [AliasAs("qn")] int quality, [AliasAs("fnval")] int streamType, + public Task> GetVideoMp4UrlByAvid(string avid, long cid, + [AliasAs("qn")] int quality, [AliasAs("fnval")] int streamType = (int)BiliVideoStreamType.Mp4, + [AliasAs("fourk")] int allow4K = 1, [AliasAs("fnver")] int streamTypeVersion = 0, string platform = "html5", + [AliasAs("high_quality")] int highQuality = 1, + [AliasAs("access_key")] string accessKey = "49fac2d6e7bb84499dffc2b4e2c94171", + [AliasAs("appkey")] string appKey = "27eb53fc9058f8c3"); + + [Get("/x/player/wbi/playurl")] + public Task> GetVideoDashUrlByBvid(string bvid, long cid, + [AliasAs("qn")] int quality, [AliasAs("fnval")] int streamType = (int)BiliVideoStreamType.Dash, + [AliasAs("fourk")] int allow4K = 1, [AliasAs("fnver")] int streamTypeVersion = 0, string platform = "html5", + [AliasAs("high_quality")] int highQuality = 1); + + [Get("/x/player/wbi/playurl")] + public Task> GetVideoDashUrlByAvid(string avid, long cid, + [AliasAs("qn")] int quality, [AliasAs("fnval")] int streamType = (int)BiliVideoStreamType.Dash, [AliasAs("fourk")] int allow4K = 1, [AliasAs("fnver")] int streamTypeVersion = 0, string platform = "html5", [AliasAs("high_quality")] int highQuality = 1, [AliasAs("access_key")] string accessKey = "49fac2d6e7bb84499dffc2b4e2c94171", diff --git a/MisakaBiliCore/Utils/NoP2PUtils.cs b/MisakaBiliCore/Utils/NoP2PUtils.cs index ca2e8d8..bd6872d 100644 --- a/MisakaBiliCore/Utils/NoP2PUtils.cs +++ b/MisakaBiliCore/Utils/NoP2PUtils.cs @@ -5,11 +5,16 @@ namespace MisakaBiliCore.Utils; public static partial class NoP2PUtils { [GeneratedRegex("(mcdn.bilivideo.(cn|com)|szbdyd.com)")] - private static partial Regex P2PRegex(); + public static partial Regex P2PRegex(); - public static string GetNoP2PUrl(string[] urls) + public static string[] GetNoP2PUrls(string[] urls) { var p2pRegex = P2PRegex(); - return urls.First(url => !p2pRegex.IsMatch(url)); + return urls.Where(url => !p2pRegex.IsMatch(url)).ToArray(); + } + + public static string GetNoP2PUrl(string[] urls) + { + return GetNoP2PUrls(urls).First(); } }