Files
PiliPlus/lib/models/video/play/url.dart
My-Responsitories 6cda3a1880 tweaks (#1862)
* opt: linter

* tweaks

* opt: TopImage

* update

* remove repaintBoundary [skip ci]

---------

Co-authored-by: dom <githubaccount56556@proton.me>
2026-03-12 15:45:18 +08:00

389 lines
9.7 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,
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;
int? lastPlayTime;
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;
}
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;
}