mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-04-19 19:01:24 +08:00
* opt: linter * tweaks * opt: TopImage * update * remove repaintBoundary [skip ci] --------- Co-authored-by: dom <githubaccount56556@proton.me>
389 lines
9.7 KiB
Dart
389 lines
9.7 KiB
Dart
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;
|
||
}
|