mirror of
https://github.com/bggRGjQaUbCoE/PiliPlus.git
synced 2026-06-18 08:20:12 +08:00
feat: video download
Signed-off-by: bggRGjQaUbCoE <githubaccount56556@proton.me>
This commit is contained in:
@@ -36,6 +36,7 @@ import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/feed_back.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/page_utils.dart' show PageUtils;
|
||||
import 'package:PiliPlus/utils/path_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/storage_pref.dart';
|
||||
@@ -55,7 +56,6 @@ import 'package:hive/hive.dart';
|
||||
import 'package:media_kit/media_kit.dart';
|
||||
import 'package:media_kit_video/media_kit_video.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class PlPlayerController {
|
||||
@@ -629,6 +629,12 @@ class PlPlayerController {
|
||||
bool _processing = false;
|
||||
bool get processing => _processing;
|
||||
|
||||
// offline
|
||||
bool isFileSource = false;
|
||||
String? dirPath;
|
||||
String? typeTag;
|
||||
int? mediaType;
|
||||
|
||||
// 初始化资源
|
||||
Future<void> setDataSource(
|
||||
DataSource dataSource, {
|
||||
@@ -655,8 +661,15 @@ class PlPlayerController {
|
||||
VideoType? videoType,
|
||||
VoidCallback? callback,
|
||||
Volume? volume,
|
||||
String? dirPath,
|
||||
String? typeTag,
|
||||
int? mediaType,
|
||||
}) async {
|
||||
try {
|
||||
this.dirPath = dirPath;
|
||||
this.typeTag = typeTag;
|
||||
this.mediaType = mediaType;
|
||||
isFileSource = dataSource.type == DataSourceType.file;
|
||||
_processing = true;
|
||||
this.isLive = isLive;
|
||||
_videoType = videoType ?? VideoType.ugc;
|
||||
@@ -723,30 +736,25 @@ class PlPlayerController {
|
||||
}
|
||||
}
|
||||
|
||||
Directory? shadersDirectory;
|
||||
Future<Directory?> copyShadersToExternalDirectory() async {
|
||||
if (shadersDirectory != null) {
|
||||
return shadersDirectory;
|
||||
}
|
||||
final manifestContent = await rootBundle.loadString('AssetManifest.json');
|
||||
final Map<String, dynamic> manifestMap = json.decode(manifestContent);
|
||||
final directory = await getApplicationSupportDirectory();
|
||||
shadersDirectory = Directory(path.join(directory.path, 'anime_shaders'));
|
||||
|
||||
if (!shadersDirectory!.existsSync()) {
|
||||
await shadersDirectory!.create(recursive: true);
|
||||
String? shadersDirPath;
|
||||
Future<String> get copyShadersToExternalDirectory async {
|
||||
if (shadersDirPath != null) {
|
||||
return shadersDirPath!;
|
||||
}
|
||||
|
||||
final shaderFiles = manifestMap.keys.where(
|
||||
(String key) =>
|
||||
key.startsWith('assets/shaders/') && key.endsWith('.glsl'),
|
||||
);
|
||||
final dir = Directory(path.join(appSupportDirPath, 'anime_shaders'));
|
||||
if (!dir.existsSync()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
// int copiedFilesCount = 0;
|
||||
final shaderFilesPath =
|
||||
(Constants.mpvAnime4KShaders + Constants.mpvAnime4KShadersLite)
|
||||
.map((e) => 'assets/shaders/$e')
|
||||
.toList();
|
||||
|
||||
for (var filePath in shaderFiles) {
|
||||
for (final filePath in shaderFilesPath) {
|
||||
final fileName = filePath.split('/').last;
|
||||
final targetFile = File(path.join(shadersDirectory!.path, fileName));
|
||||
final targetFile = File(path.join(dir.path, fileName));
|
||||
if (targetFile.existsSync()) {
|
||||
continue;
|
||||
}
|
||||
@@ -755,12 +763,11 @@ class PlPlayerController {
|
||||
final data = await rootBundle.load(filePath);
|
||||
final List<int> bytes = data.buffer.asUint8List();
|
||||
await targetFile.writeAsBytes(bytes);
|
||||
// copiedFilesCount++;
|
||||
} catch (e) {
|
||||
if (kDebugMode) debugPrint('$e');
|
||||
}
|
||||
}
|
||||
return shadersDirectory;
|
||||
return shadersDirPath = dir.path;
|
||||
}
|
||||
|
||||
late final isAnim = _pgcType == 1 || _pgcType == 4;
|
||||
@@ -786,8 +793,8 @@ class PlPlayerController {
|
||||
'change-list',
|
||||
'glsl-shaders',
|
||||
'set',
|
||||
Utils.buildShadersAbsolutePath(
|
||||
(await copyShadersToExternalDirectory())?.path ?? '',
|
||||
PathUtils.buildShadersAbsolutePath(
|
||||
await copyShadersToExternalDirectory,
|
||||
Constants.mpvAnime4KShadersLite,
|
||||
),
|
||||
]);
|
||||
@@ -796,8 +803,8 @@ class PlPlayerController {
|
||||
'change-list',
|
||||
'glsl-shaders',
|
||||
'set',
|
||||
Utils.buildShadersAbsolutePath(
|
||||
(await copyShadersToExternalDirectory())?.path ?? '',
|
||||
PathUtils.buildShadersAbsolutePath(
|
||||
await copyShadersToExternalDirectory,
|
||||
Constants.mpvAnime4KShaders,
|
||||
),
|
||||
]);
|
||||
@@ -861,29 +868,19 @@ class PlPlayerController {
|
||||
}
|
||||
|
||||
// 音轨
|
||||
if (dataSource.audioSource?.isNotEmpty == true) {
|
||||
await pp.setProperty(
|
||||
'audio-files',
|
||||
Platform.isWindows
|
||||
? dataSource.audioSource!.replaceAll(';', '\\;')
|
||||
: dataSource.audioSource!.replaceAll(':', '\\:'),
|
||||
);
|
||||
late final String audioUri;
|
||||
if (isFileSource) {
|
||||
audioUri = onlyPlayAudio.value || mediaType == 1
|
||||
? ''
|
||||
: path.join(dirPath!, typeTag!, PathUtils.audioNameType2);
|
||||
} else if (dataSource.audioSource?.isNotEmpty == true) {
|
||||
audioUri = Platform.isWindows
|
||||
? dataSource.audioSource!.replaceAll(';', '\\;')
|
||||
: dataSource.audioSource!.replaceAll(':', '\\:');
|
||||
} else {
|
||||
await pp.setProperty('audio-files', '');
|
||||
}
|
||||
|
||||
// 字幕
|
||||
if (dataSource.subFiles?.isNotEmpty == true) {
|
||||
await pp.setProperty(
|
||||
'sub-files',
|
||||
Platform.isWindows
|
||||
? dataSource.subFiles!.replaceAll(';', '\\;')
|
||||
: dataSource.subFiles!.replaceAll(':', '\\:'),
|
||||
);
|
||||
await pp.setProperty("subs-with-matching-audio", "no");
|
||||
await pp.setProperty("sub-forced-only", "yes");
|
||||
await pp.setProperty("blend-subtitles", "video");
|
||||
audioUri = '';
|
||||
}
|
||||
await pp.setProperty('audio-files', audioUri);
|
||||
|
||||
_videoController ??= VideoController(
|
||||
player,
|
||||
@@ -928,41 +925,39 @@ class PlPlayerController {
|
||||
filters = null;
|
||||
}
|
||||
|
||||
if (kDebugMode) debugPrint(filters.toString());
|
||||
// if (kDebugMode) debugPrint(filters.toString());
|
||||
|
||||
if (dataSource.type == DataSourceType.asset) {
|
||||
final assetUrl = dataSource.videoSource!.startsWith("asset://")
|
||||
? dataSource.videoSource!
|
||||
: "asset://${dataSource.videoSource!}";
|
||||
await player.open(
|
||||
Media(
|
||||
assetUrl,
|
||||
httpHeaders: dataSource.httpHeaders,
|
||||
start: seekTo,
|
||||
extras: filters,
|
||||
),
|
||||
play: false,
|
||||
late final String videoUri;
|
||||
if (isFileSource) {
|
||||
videoUri = path.join(
|
||||
dirPath!,
|
||||
typeTag!,
|
||||
mediaType == 1
|
||||
? PathUtils.videoNameType1
|
||||
: onlyPlayAudio.value
|
||||
? PathUtils.audioNameType2
|
||||
: PathUtils.videoNameType2,
|
||||
);
|
||||
} else {
|
||||
await player.open(
|
||||
Media(
|
||||
dataSource.videoSource!,
|
||||
httpHeaders: dataSource.httpHeaders,
|
||||
start: seekTo,
|
||||
extras: filters,
|
||||
),
|
||||
play: false,
|
||||
);
|
||||
videoUri = dataSource.videoSource!;
|
||||
}
|
||||
// 音轨
|
||||
// player.setAudioTrack(
|
||||
// AudioTrack.uri(dataSource.audioSource!),
|
||||
// );
|
||||
await player.open(
|
||||
Media(
|
||||
videoUri,
|
||||
httpHeaders: dataSource.httpHeaders,
|
||||
start: seekTo,
|
||||
extras: filters,
|
||||
),
|
||||
play: false,
|
||||
);
|
||||
|
||||
return player;
|
||||
}
|
||||
|
||||
Future<bool> refreshPlayer() async {
|
||||
if (isFileSource) {
|
||||
return true;
|
||||
}
|
||||
if (_videoPlayerController == null) {
|
||||
// SmartDialog.showToast('视频播放器为空,请重新进入本页面');
|
||||
return false;
|
||||
@@ -1123,7 +1118,12 @@ class PlPlayerController {
|
||||
debugPrint(log.toString());
|
||||
})),
|
||||
videoPlayerController!.stream.error.listen((String event) {
|
||||
debugPrint('MPV Exception: $event');
|
||||
if (kDebugMode) {
|
||||
debugPrint('MPV Exception: $event');
|
||||
}
|
||||
if (isFileSource && event.startsWith("Failed to open file")) {
|
||||
return;
|
||||
}
|
||||
if (isLive) {
|
||||
if (event.startsWith('tcp: ffurl_read returned ') ||
|
||||
event.startsWith("Failed to open https://") ||
|
||||
|
||||
@@ -1,54 +1,37 @@
|
||||
import 'dart:io';
|
||||
|
||||
/// The way in which the video was originally loaded.
|
||||
///
|
||||
/// This has nothing to do with the video's file type. It's just the place
|
||||
/// from which the video is fetched from.
|
||||
enum DataSourceType {
|
||||
/// The video was included in the app's asset files.
|
||||
asset,
|
||||
|
||||
/// The video was downloaded from the internet.
|
||||
network,
|
||||
|
||||
/// The video was loaded off of the local filesystem.
|
||||
file,
|
||||
|
||||
/// The video is available via contentUri. Android only.
|
||||
contentUri,
|
||||
}
|
||||
|
||||
class DataSource {
|
||||
File? file;
|
||||
String? videoSource;
|
||||
String? audioSource;
|
||||
String? subFiles;
|
||||
DataSourceType type;
|
||||
Map<String, String>? httpHeaders; // for headers
|
||||
|
||||
DataSource({
|
||||
this.file,
|
||||
this.videoSource,
|
||||
this.audioSource,
|
||||
this.subFiles,
|
||||
required this.type,
|
||||
this.httpHeaders,
|
||||
}) : assert(
|
||||
(type == DataSourceType.file && file != null) || videoSource != null,
|
||||
);
|
||||
});
|
||||
|
||||
DataSource copyWith({
|
||||
File? file,
|
||||
String? videoSource,
|
||||
String? audioSource,
|
||||
String? subFiles,
|
||||
DataSourceType? type,
|
||||
Map<String, String>? httpHeaders,
|
||||
}) {
|
||||
return DataSource(
|
||||
file: file ?? this.file,
|
||||
videoSource: videoSource ?? this.videoSource,
|
||||
audioSource: audioSource ?? this.audioSource,
|
||||
subFiles: subFiles ?? this.subFiles,
|
||||
type: type ?? this.type,
|
||||
httpHeaders: httpHeaders ?? this.httpHeaders,
|
||||
);
|
||||
|
||||
@@ -19,6 +19,7 @@ import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart';
|
||||
import 'package:PiliPlus/models/common/super_resolution_type.dart';
|
||||
import 'package:PiliPlus/models/common/video/video_quality.dart';
|
||||
import 'package:PiliPlus/models/video/play/url.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc;
|
||||
import 'package:PiliPlus/models_new/video/video_detail/episode.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/section.dart';
|
||||
import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart';
|
||||
@@ -51,6 +52,7 @@ import 'package:PiliPlus/utils/duration_utils.dart';
|
||||
import 'package:PiliPlus/utils/extension.dart';
|
||||
import 'package:PiliPlus/utils/id_utils.dart';
|
||||
import 'package:PiliPlus/utils/image_utils.dart';
|
||||
import 'package:PiliPlus/utils/path_utils.dart';
|
||||
import 'package:PiliPlus/utils/storage.dart';
|
||||
import 'package:PiliPlus/utils/storage_key.dart';
|
||||
import 'package:PiliPlus/utils/utils.dart';
|
||||
@@ -98,7 +100,14 @@ class PLVideoPlayer extends StatefulWidget {
|
||||
final Widget headerControl;
|
||||
final Widget? bottomControl;
|
||||
final Widget? danmuWidget;
|
||||
final void Function([int?, UgcSeason?, dynamic, String?, int?, int?])?
|
||||
final void Function([
|
||||
int?,
|
||||
UgcSeason?,
|
||||
List<ugc.BaseEpisodeItem>?,
|
||||
String?,
|
||||
int?,
|
||||
int?,
|
||||
])?
|
||||
showEpisodes;
|
||||
final VoidCallback? showViewPoints;
|
||||
final Color fill;
|
||||
@@ -517,6 +526,10 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
color: Colors.white,
|
||||
),
|
||||
onTap: () {
|
||||
if (videoDetailController.isFileSource) {
|
||||
// TODO
|
||||
return;
|
||||
}
|
||||
// part -> playAll -> season(pgc)
|
||||
if (isPlayAll && !isPart) {
|
||||
widget.showEpisodes?.call();
|
||||
@@ -525,7 +538,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
int? index;
|
||||
int currentCid = plPlayerController.cid!;
|
||||
String bvid = plPlayerController.bvid;
|
||||
List episodes = [];
|
||||
List<ugc.BaseEpisodeItem> episodes = [];
|
||||
if (isSeason) {
|
||||
final List<SectionItem> sections = videoDetail.ugcSeason!.sections!;
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
@@ -836,10 +849,12 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
),
|
||||
};
|
||||
|
||||
final isNotFileSource = !plPlayerController.isFileSource;
|
||||
|
||||
List<BottomControlType> userSpecifyItemLeft = [
|
||||
BottomControlType.playOrPause,
|
||||
BottomControlType.time,
|
||||
if (anySeason) ...[
|
||||
if (!isNotFileSource || anySeason) ...[
|
||||
BottomControlType.pre,
|
||||
BottomControlType.next,
|
||||
],
|
||||
@@ -848,15 +863,19 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
final flag =
|
||||
isFullScreen || plPlayerController.isDesktopPip || maxWidth >= 500;
|
||||
List<BottomControlType> userSpecifyItemRight = [
|
||||
if (plPlayerController.showDmChart) BottomControlType.dmChart,
|
||||
if (isNotFileSource && plPlayerController.showDmChart)
|
||||
BottomControlType.dmChart,
|
||||
if (plPlayerController.isAnim) BottomControlType.superResolution,
|
||||
if (plPlayerController.showViewPoints) BottomControlType.viewPoints,
|
||||
if (anySeason) BottomControlType.episode,
|
||||
if (isNotFileSource && plPlayerController.showViewPoints)
|
||||
BottomControlType.viewPoints,
|
||||
if (isNotFileSource || anySeason) BottomControlType.episode,
|
||||
if (flag) BottomControlType.fit,
|
||||
BottomControlType.aiTranslate,
|
||||
BottomControlType.subtitle,
|
||||
if (isNotFileSource) ...[
|
||||
BottomControlType.aiTranslate,
|
||||
BottomControlType.subtitle,
|
||||
],
|
||||
BottomControlType.speed,
|
||||
if (flag) BottomControlType.qa,
|
||||
if (isNotFileSource && flag) BottomControlType.qa,
|
||||
if (!plPlayerController.isDesktopPip) BottomControlType.fullscreen,
|
||||
];
|
||||
|
||||
@@ -1034,7 +1053,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
plPlayerController
|
||||
..onUpdatedSliderProgress(result)
|
||||
..onChangedSliderStart();
|
||||
if (plPlayerController.showSeekPreview &&
|
||||
if (!plPlayerController.isFileSource &&
|
||||
plPlayerController.showSeekPreview &&
|
||||
plPlayerController.cancelSeek != true) {
|
||||
plPlayerController.updatePreviewIndex(newPos ~/ 1000);
|
||||
}
|
||||
@@ -1285,7 +1305,8 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
plPlayerController
|
||||
..onUpdatedSliderProgress(result)
|
||||
..onChangedSliderStart();
|
||||
if (plPlayerController.showSeekPreview &&
|
||||
if (!plPlayerController.isFileSource &&
|
||||
plPlayerController.showSeekPreview &&
|
||||
plPlayerController.cancelSeek != true) {
|
||||
plPlayerController.updatePreviewIndex(newPos ~/ 1000);
|
||||
}
|
||||
@@ -2186,7 +2207,7 @@ class _PLVideoPlayerState extends State<PLVideoPlayer>
|
||||
final progress = 0.0.obs;
|
||||
final name =
|
||||
'${ctr.cid}-${segment.first.toStringAsFixed(3)}_${segment.second.toStringAsFixed(3)}.webp';
|
||||
final file = '${await Utils.temporaryDirectory}/$name';
|
||||
final file = '$tmpDirPath/$name';
|
||||
|
||||
final mpv = MpvConvertWebp(
|
||||
url!,
|
||||
|
||||
@@ -45,7 +45,7 @@ class BottomControl extends StatelessWidget {
|
||||
}
|
||||
|
||||
void onDragUpdate(ThumbDragDetails duration, int max) {
|
||||
if (controller.showSeekPreview) {
|
||||
if (!controller.isFileSource && controller.showSeekPreview) {
|
||||
controller.updatePreviewIndex(
|
||||
duration.timeStamp.inSeconds,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user