import 'dart:math' show max, min; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models_new/sponsor_block/segment_item.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:flutter/foundation.dart' show kDebugMode; class PlayUrlModel { PlayUrlModel({ this.from, this.result, this.message, this.quality, this.format, this.timeLength, this.acceptFormat, this.acceptDesc, this.acceptQuality, this.videoCodecid, this.seekParam, this.seekType, this.dash, this.supportFormats, this.lastPlayTime, this.lastPlayCid, }); String? from; String? result; String? message; int? quality; String? format; int? timeLength; String? acceptFormat; List? acceptDesc; List? acceptQuality; int? videoCodecid; String? seekParam; String? seekType; Dash? dash; List? durl; List? supportFormats; Volume? volume; int? lastPlayTime; int? lastPlayCid; String? curLanguage; Language? language; List? clipInfoList; PlayUrlModel.fromJson(Map json) { from = json['from']; result = json['result']; message = json['message']; quality = json['quality']; format = json['format']; timeLength = json['timelength']; acceptFormat = json['accept_format']; acceptDesc = json['accept_description']; acceptQuality = (json['accept_quality'] as List?) ?.map((e) => e as int) .toList(); videoCodecid = json['video_codecid']; seekParam = json['seek_param']; seekType = json['seek_type']; dash = json['dash'] != null ? Dash.fromJson(json['dash']) : null; durl = (json['durl'] as List?)?.map((e) => Durl.fromJson(e)).toList(); supportFormats = (json['support_formats'] as List?) ?.map((e) => FormatItem.fromJson(e)) .toList(); volume = json['volume'] == null ? null : Volume.fromJson(json['volume']); lastPlayTime = json['last_play_time']; lastPlayCid = json['last_play_cid']; curLanguage = json['cur_language']; language = json['language'] == null ? null : Language.fromJson(json['language']); // debug // final clipInfoList = [ // { // "start": 0, // "end": 150, // "clipType": "CLIP_TYPE_OP", // }, // { // "start": timeLength! ~/ 1000 - 150, // "end": timeLength! ~/ 1000, // "clipType": "CLIP_TYPE_ED", // }, // ]; try { final List? clipInfoList = json['clip_info_list']; if (clipInfoList != null && clipInfoList.isNotEmpty) { this.clipInfoList = clipInfoList .map((e) => SegmentItemModel.fromPgcJson(e, timeLength)) .toList(); } } catch (_) { if (kDebugMode) rethrow; } } } class Language { Language({ this.support, this.items, }); bool? support; List? items; Language.fromJson(Map json) { support = json['support']; items = (json['items'] as List?)?.map((e) => LanguageItem.fromJson(e)).toList() ?..sort((a, b) { final aHasZh = a.lang?.contains('zh') ?? false; final bHasZh = b.lang?.contains('zh') ?? false; if (aHasZh != bHasZh) return aHasZh ? -1 : 1; if (a.isAi != b.isAi) return a.isAi ? 1 : -1; return 0; }); } } class LanguageItem { LanguageItem({ this.lang, this.title, this.subtitleLang, }); String? lang; String? title; String? subtitleLang; bool isAi = false; LanguageItem.fromJson(Map json) { lang = json['lang']; isAi = json['production_type'] == 2; title = '${json['title']}${isAi ? '(AI)' : ''}'; subtitleLang = json['subtitle_lang']; } } class Dash { Dash({ this.duration, this.minBufferTime, this.video, this.audio, }); int? duration; double? minBufferTime; List? video; List? audio; Dash.fromJson(Map json) { duration = json['duration']; minBufferTime = json['minBufferTime']; video = (json['video'] as List?) ?.map((e) => VideoItem.fromJson(e)) .toList(); final audio = [ if (json['flac']?['audio'] case Map flac) AudioItem.fromJson(flac), if (json['dolby']?['audio'] case List list) ...list.map((e) => AudioItem.fromJson(e)), if (json['audio'] case List list) ...list.map((e) => AudioItem.fromJson(e)), ]; this.audio = audio.isEmpty ? null : audio; } } class Durl { int? order; int? length; int? size; String? ahead; String? vhead; String? url; List? backupUrl; Durl({ this.order, this.length, this.size, this.ahead, this.vhead, this.url, this.backupUrl, }); factory Durl.fromJson(Map json) { return Durl( order: json['order'], length: json['length'], size: json['size'], ahead: json['ahead'], vhead: json['vhead'], url: json['url'], backupUrl: (json['backup_url'] as List?)?.fromCast(), ); } Iterable get playUrls sync* { if (url?.isNotEmpty == true) yield url!; if (backupUrl?.isNotEmpty == true) yield* backupUrl!; } } abstract class BaseItem { int? id; String? baseUrl; List? backupUrl; int? bandWidth; String? mimeType; String? codecs; int? width; int? height; String? frameRate; String? sar; int? startWithSap; Map? segmentBase; int? codecid; BaseItem({ this.id, this.baseUrl, this.backupUrl, this.bandWidth, this.mimeType, this.codecs, this.width, this.height, this.frameRate, this.sar, this.startWithSap, this.segmentBase, this.codecid, }); BaseItem.fromJson(Map json) { id = json['id']; baseUrl = json['baseUrl'] ?? json['base_url']; backupUrl = ((json['backupUrl'] ?? json['backup_url']) as List?) ?.fromCast(); bandWidth = json['bandWidth'] ?? json['bandwidth']; mimeType = json['mime_type']; codecs = json['codecs']; width = json['width']; height = json['height']; frameRate = json['frameRate'] ?? json['frame_rate']; sar = json['sar']; startWithSap = json['startWithSap'] ?? json['start_with_sap']; segmentBase = json['segmentBase'] ?? json['segment_base']; codecid = json['codecid']; } Iterable get playUrls sync* { if (baseUrl?.isNotEmpty == true) yield baseUrl!; if (backupUrl?.isNotEmpty == true) yield* backupUrl!; } } class VideoItem extends BaseItem { late VideoQuality quality; VideoItem({ super.id, super.baseUrl, super.backupUrl, super.bandWidth, super.mimeType, super.codecs, super.width, super.height, super.frameRate, super.sar, super.startWithSap, super.segmentBase, super.codecid, required this.quality, }); VideoItem.fromJson(Map json) : super.fromJson(json) { quality = VideoQuality.fromCode(json['id']); } } class AudioItem extends BaseItem { late String quality; AudioItem(); AudioItem.fromJson(Map json) : super.fromJson(json) { quality = AudioQuality.fromCode(json['id']).desc; } } class FormatItem { FormatItem({ this.quality, this.format, this.newDesc, this.displayDesc, this.codecs, }); int? quality; String? format; String? newDesc; String? displayDesc; List? codecs; FormatItem.fromJson(Map json) { quality = json['quality']; format = json['format']; newDesc = json['new_description']; displayDesc = json['display_desc']; codecs = (json['codecs'] as List?)?.fromCast(); } } class Volume { Volume({ required this.measuredI, required this.measuredLra, required this.measuredTp, required this.measuredThreshold, required this.targetOffset, required this.targetI, required this.targetTp, // required this.multiSceneArgs, }); final num measuredI; final num measuredLra; final num measuredTp; final num measuredThreshold; final num targetOffset; final num targetI; final num targetTp; // final MultiSceneArgs? multiSceneArgs; // FFmpeg loudnorm 滤镜的标准有效范围(https://ffmpeg.org/ffmpeg-filters.html#loudnorm) static const double minTpValue = -9.0; static const double maxTpValue = 0.0; factory Volume.fromJson(Map json) { return Volume( measuredI: json["measured_i"] ?? 0, measuredLra: json["measured_lra"] ?? 0, measuredTp: json["measured_tp"] ?? 0, measuredThreshold: json["measured_threshold"] ?? 0, targetOffset: json["target_offset"] ?? 0, targetI: json["target_i"] ?? 0, targetTp: json["target_tp"] ?? 0, // multiSceneArgs: json["multi_scene_args"] == null ? null : MultiSceneArgs.fromJson(json["multi_scene_args"]), ); } String format(Map config) { final lra = max(config['lra'] ?? 11, measuredLra); num i = config['i'] ?? targetI; final tp = min( config['tp'] ?? targetTp, measuredTp, ).clamp(minTpValue, maxTpValue); final offset = config['offset'] ?? targetOffset; num measuredI = this.measuredI; if (measuredI > 0) { i -= measuredI; measuredI = 0; } return 'LRA=$lra:I=$i:TP=$tp:offset=$offset:linear=true:measured_I=$measuredI:measured_LRA=$measuredLra:measured_TP=$measuredTp:measured_thresh=$measuredThreshold'; } bool get isNotEmpty => measuredI != 0 || measuredLra != 0 || measuredTp != 0 || measuredThreshold != 0; }