Files
PiliPlus/lib/models/video/play/url.dart
2026-06-13 10:02:44 +08:00

404 lines
10 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = 0,
this.lastPlayCid,
});
String? from;
String? result;
String? message;
int? quality;
String? format;
int? timeLength;
String? acceptFormat;
List<dynamic>? acceptDesc;
List<int>? acceptQuality;
int? videoCodecid;
String? seekParam;
String? seekType;
Dash? dash;
List<Durl>? durl;
List<FormatItem>? supportFormats;
Volume? volume;
late int _lastPlayTime;
int get lastPlayTime => _lastPlayTime;
set lastPlayTime(int? value) {
if (value != null && value > 0) {
_lastPlayTime = value;
} else {
_lastPlayTime = 0;
}
}
int? lastPlayCid;
String? curLanguage;
Language? language;
List<SegmentItemModel>? clipInfoList;
PlayUrlModel.fromJson(Map<String, dynamic> 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<int>((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<Durl>((e) => Durl.fromJson(e)).toList();
supportFormats = (json['support_formats'] as List?)
?.map<FormatItem>((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<LanguageItem>? items;
Language.fromJson(Map<String, dynamic> 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<String, dynamic> 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<VideoItem>? video;
List<AudioItem>? audio;
Dash.fromJson(Map<String, dynamic> json) {
duration = json['duration'];
minBufferTime = json['minBufferTime'];
video = (json['video'] as List?)
?.map<VideoItem>((e) => VideoItem.fromJson(e))
.toList();
final audio = [
if (json['flac']?['audio'] case Map<String, dynamic> 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<String>? backupUrl;
Durl({
this.order,
this.length,
this.size,
this.ahead,
this.vhead,
this.url,
this.backupUrl,
});
factory Durl.fromJson(Map<String, dynamic> 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<String>(),
);
}
Iterable<String> get playUrls sync* {
if (url?.isNotEmpty == true) yield url!;
if (backupUrl?.isNotEmpty == true) yield* backupUrl!;
}
}
abstract class BaseItem {
int? id;
String? baseUrl;
List<String>? 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<String, dynamic> json) {
id = json['id'];
baseUrl = json['baseUrl'] ?? json['base_url'];
backupUrl = ((json['backupUrl'] ?? json['backup_url']) as List?)
?.fromCast<String>();
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<String> 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<String, dynamic> json) : super.fromJson(json) {
quality = VideoQuality.fromCode(json['id']);
}
}
class AudioItem extends BaseItem {
late String quality;
AudioItem();
AudioItem.fromJson(Map<String, dynamic> 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<String>? codecs;
FormatItem.fromJson(Map<String, dynamic> json) {
quality = json['quality'];
format = json['format'];
newDesc = json['new_description'];
displayDesc = json['display_desc'];
codecs = (json['codecs'] as List?)?.fromCast<String>();
}
}
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<String, dynamic> 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<String, num> 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;
}
num measuredThreshold = this.measuredThreshold;
if (measuredThreshold > 0) {
measuredThreshold = 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;
}