Commit fac3e49d by tanghuan

优化

1 parent 0925b7fc
...@@ -194,14 +194,14 @@ class WebCubit extends Cubit<WebState> { ...@@ -194,14 +194,14 @@ class WebCubit extends Cubit<WebState> {
var versionConfig = await _getVersionConfig(); var versionConfig = await _getVersionConfig();
var correctVersion = versionConfig['version'] as String; var correctVersion = versionConfig['version'] as String;
var downloadUrl = versionConfig['zip'] as String; var downloadUrl = versionConfig['zip'] as String;
var urgency = versionConfig['urgency'] as int? ?? 0; var force = versionConfig['force'] as String;
// 当前使用的H5版本 // 当前使用的H5版本
var curVersion = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version; var curVersion = getIt.get<SharedPreferences>().getString('h5_version') ?? Constant.h5Version;
// 版本不一致则需要升级 // 版本不一致则需要升级
if (curVersion != correctVersion) { if (curVersion != correctVersion) {
if (urgency == 1) { if (force == "1") {
// 一直等待升级完成 // 一直等待升级完成
// 遮罩界面 // 遮罩界面
emit(state.copyWith(isUpgrading: true)); emit(state.copyWith(isUpgrading: true));
...@@ -256,11 +256,13 @@ class WebCubit extends Cubit<WebState> { ...@@ -256,11 +256,13 @@ class WebCubit extends Cubit<WebState> {
} }
String version = response.data['version'] as String; String version = response.data['version'] as String;
String force = response.data['force'] as String;
String zip = response.data['zip'] as String; String zip = response.data['zip'] as String;
return { return {
'version': version, 'version': version,
'zip': 'http://192.168.2.177/1.0.0.zip', 'force': force,
// 'zip': zip, // 'zip': 'http://192.168.2.177/1.0.0.zip',
'zip': '$zip$version.zip',
}; };
} finally { } finally {
dio.close(force: true); dio.close(force: true);
...@@ -481,11 +483,8 @@ class WebCubit extends Cubit<WebState> { ...@@ -481,11 +483,8 @@ class WebCubit extends Cubit<WebState> {
_controller.reload(); _controller.reload();
} }
///
/// 1 清理非 h5_version 的缓存
/// 2 清理文件目录和缓存目录(不包括解压的H5资源目录)
///
Future<void> clearStorage() async { Future<void> clearStorage() async {
// 1 清理非 h5_version 的缓存
var sharedPreferences = getIt.get<SharedPreferences>(); var sharedPreferences = getIt.get<SharedPreferences>();
sharedPreferences.getKeys().forEach((key) { sharedPreferences.getKeys().forEach((key) {
if (!key.startsWith('h5_version')) { if (!key.startsWith('h5_version')) {
...@@ -493,26 +492,34 @@ class WebCubit extends Cubit<WebState> { ...@@ -493,26 +492,34 @@ class WebCubit extends Cubit<WebState> {
} }
}); });
var version = sharedPreferences.getString('h5_version') ?? Constant.h5Version; // 2 清理 http_dist_assets 下的非当前版本号的文件和目录
var dir = await getApplicationSupportDirectory(); var dir = await getApplicationSupportDirectory();
String httpDirPath = '${dir.path}/${Constant.h5DistDir}'; var httpDir = Directory('${dir.path}/${Constant.h5DistDir}');
if (httpDir.existsSync()) {
var httpDir = Directory(httpDirPath); var version = sharedPreferences.getString('h5_version') ?? Constant.h5Version;
if (!httpDir.existsSync()) {
return; await for (final FileSystemEntity entity in httpDir.list()) {
if (entity is Directory) {
// 删除目录
if (!entity.path.endsWith(version)) {
await entity.delete(recursive: true);
}
} else if (entity is File) {
// 删除文件
if (!entity.path.endsWith('$version.zip')) {
await entity.delete();
}
}
}
} }
// 查询目录下的所有文件和目录 // 3 清理临时目录下的所有文件和目录
List<FileSystemEntity> entities = await Directory(httpDirPath).list().toList(); var tempDir = await getTemporaryDirectory();
for (var entity in entities) { if (tempDir.existsSync()) {
if (entity is Directory) { await for (final FileSystemEntity entity in tempDir.list()) {
// 删除目录 if (entity is Directory) {
if (!entity.path.endsWith(version)) {
await entity.delete(recursive: true); await entity.delete(recursive: true);
} } else {
} else if (entity is File) {
// 删除文件
if (!entity.path.endsWith('$version.zip')) {
await entity.delete(); await entity.delete();
} }
} }
......
import 'dart:io'; import 'dart:io';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart';
import 'package:appframe/utils/video_util.dart'; import 'package:appframe/utils/video_util.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
...@@ -26,13 +27,7 @@ class CompressImageHandler extends MessageHandler { ...@@ -26,13 +27,7 @@ class CompressImageHandler extends MessageHandler {
throw Exception('参数错误'); throw Exception('参数错误');
} }
var compressedWidth = params['compressedWidth'] as int?; var compressedWidth = params['compressedWidth'] as int?;
// if (compressedWidth == null) {
// throw Exception('参数错误');
// }
var compressedHeight = params['compressedHeight'] as int?; var compressedHeight = params['compressedHeight'] as int?;
// if (compressedHeight == null) {
// throw Exception('参数错误');
// }
if (compressedWidth == null && compressedHeight == null) { if (compressedWidth == null && compressedHeight == null) {
throw Exception('参数错误'); throw Exception('参数错误');
...@@ -40,8 +35,8 @@ class CompressImageHandler extends MessageHandler { ...@@ -40,8 +35,8 @@ class CompressImageHandler extends MessageHandler {
if (compressedWidth == null) { if (compressedWidth == null) {
compressedWidth = compressedHeight; compressedWidth = compressedHeight;
} else if (compressedHeight == null) { } else {
compressedHeight = compressedWidth; compressedHeight ??= compressedWidth;
} }
// 获取后缀名 // 获取后缀名
...@@ -128,8 +123,19 @@ class CompressVideoHandler extends MessageHandler { ...@@ -128,8 +123,19 @@ class CompressVideoHandler extends MessageHandler {
} }
print('原视频大小:${originFile.lengthSync()}'); print('原视频大小:${originFile.lengthSync()}');
String? mimeType = await FileTypeUtil.getMimeType(originFile);
if (!(mimeType?.startsWith('video/') ?? false)) {
throw Exception('非视频文件');
}
final outputPath = '${tempDir.path}/${Uuid().v4()}.mp4'; final outputPath = '${tempDir.path}/${Uuid().v4()}.mp4';
var result = await VideoUtil.compressVideo(srcPath, outputPath, quality);
bool result;
if (mimeType != 'video/mp4') {
result = await VideoUtil.convertToMp4(srcPath, outputPath);
} else {
result = await VideoUtil.compressVideo(srcPath, outputPath, quality);
}
if (!result) { if (!result) {
throw Exception('视频压缩失败'); throw Exception('视频压缩失败');
} }
...@@ -139,32 +145,5 @@ class CompressVideoHandler extends MessageHandler { ...@@ -139,32 +145,5 @@ class CompressVideoHandler extends MessageHandler {
"tempFilePath": '/temp$outputPath', "tempFilePath": '/temp$outputPath',
"size": File(outputPath).lengthSync(), "size": File(outputPath).lengthSync(),
}; };
// VideoQuality videoQuality;
// switch (quality) {
// case 'low':
// videoQuality = VideoQuality.LowQuality;
// break;
// case 'middle':
// videoQuality = VideoQuality.MediumQuality;
// break;
// case 'high':
// videoQuality = VideoQuality.HighestQuality;
// break;
// default:
// throw Exception('参数错误');
// }
//
// final mediaInfo = await VideoCompress.compressVideo(
// srcPath,
// quality: videoQuality,
// deleteOrigin: false,
// includeAudio: true,
// );
//
// return {
// "tempFilePath": "/temp${mediaInfo!.path}",
// "size": mediaInfo.filesize,
// };
} }
} }
...@@ -3,9 +3,11 @@ import 'dart:io'; ...@@ -3,9 +3,11 @@ import 'dart:io';
import 'package:appframe/services/dispatcher.dart'; import 'package:appframe/services/dispatcher.dart';
import 'package:appframe/utils/file_type_util.dart'; import 'package:appframe/utils/file_type_util.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:ffmpeg_kit_flutter_new/ffprobe_kit.dart';
import 'package:ffmpeg_kit_flutter_new/media_information_session.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:video_compress/video_compress.dart';
class VideoInfoHandler extends MessageHandler { class VideoInfoHandler extends MessageHandler {
@override @override
...@@ -15,8 +17,26 @@ class VideoInfoHandler extends MessageHandler { ...@@ -15,8 +17,26 @@ class VideoInfoHandler extends MessageHandler {
} }
final url = params['url'] as String; final url = params['url'] as String;
final filePath = await _getFilePath(url);
final file = File(filePath);
if (!file.existsSync()) {
throw Exception('视频文件不存在');
}
// 使用 ffmpeg_kit_flutter_new 获取视频信息
final mediaInfoSession = await FFprobeKit.getMediaInformation(filePath);
final returnCode = await mediaInfoSession.getReturnCode();
if (!ReturnCode.isSuccess(returnCode)) {
throw Exception('获取视频信息失败');
}
final result = await _extractVideoInfo(mediaInfoSession, file, filePath);
return result;
}
String filePath; /// 根据URL获取文件路径,如果URL是网络地址则下载到本地
Future<String> _getFilePath(String url) async {
if (url.startsWith('http')) { if (url.startsWith('http')) {
// 获取后缀名 // 获取后缀名
String ext = path.extension(url); String ext = path.extension(url);
...@@ -30,18 +50,41 @@ class VideoInfoHandler extends MessageHandler { ...@@ -30,18 +50,41 @@ class VideoInfoHandler extends MessageHandler {
throw Exception('文件下载失败'); throw Exception('文件下载失败');
} }
filePath = targetPath; return targetPath;
} else { } else {
filePath = url; return url;
} }
}
final file = File(filePath); /// 提取视频信息
if (!file.existsSync()) { Future<Map<String, dynamic>> _extractVideoInfo(
throw Exception('视频文件不存在'); MediaInformationSession mediaInfoSession, File file, String filePath) async {
final mediaInformation = mediaInfoSession.getMediaInformation();
if (mediaInformation == null) {
throw Exception('获取视频信息失败');
}
// 获取视频时长
final durationStr = mediaInformation.getDuration();
if (durationStr == null) {
throw Exception('获取视频信息失败');
}
var duration = (double.tryParse(durationStr) ?? 0).ceil();
// 获取视频流信息
final videoStreams = mediaInformation
.getStreams()
.where((stream) => stream.getAllProperties() != null && stream.getAllProperties()!['codec_type'] == 'video')
.toList();
if (videoStreams.isEmpty) {
throw Exception('获取视频信息失败');
} }
// 使用video_compress获取视频信息 final videoStream = videoStreams[0];
final mediaInfo = await VideoCompress.getMediaInfo(filePath); final properties = videoStream.getAllProperties();
int width = properties!['width'] ?? 0;
int height = properties['height'] ?? 0;
// 获取文件大小 // 获取文件大小
final size = await file.length(); final size = await file.length();
...@@ -52,10 +95,10 @@ class VideoInfoHandler extends MessageHandler { ...@@ -52,10 +95,10 @@ class VideoInfoHandler extends MessageHandler {
return { return {
'tempFilePath': '/temp$filePath', 'tempFilePath': '/temp$filePath',
'width': mediaInfo.width ?? 0, 'width': width,
'height': mediaInfo.height ?? 0, 'height': height,
'type': fileExtension, 'type': fileExtension,
'duration': (mediaInfo.duration ?? 0) / 1000, // 转换为秒 'duration': duration, // 已经是秒单位
'size': size, 'size': size,
}; };
} }
......
import 'dart:io'; import 'dart:io';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:flutter_image_compress/flutter_image_compress.dart'; import 'package:flutter_image_compress/flutter_image_compress.dart';
import 'package:video_compress/video_compress.dart';
/// 缩略图工具类 /// 缩略图工具类
/// ///
...@@ -28,28 +29,29 @@ class ThumbnailUtil { ...@@ -28,28 +29,29 @@ class ThumbnailUtil {
/// 返回缩略图路径 /// 返回缩略图路径
static Future<String?> genVideoThumbnail(String videoPath, Directory dir) async { static Future<String?> genVideoThumbnail(String videoPath, Directory dir) async {
try { try {
var fileThumbnail = await VideoCompress.getFileThumbnail(videoPath, quality: 50, position: -1); final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';
return fileThumbnail.path;
// 使用 ffmpeg_kit_flutter_new 生成视频缩略图
// 构建FFmpeg命令行参数
String cmd = '-i "$videoPath" ' // 指定输入文件路径
'-ss 1 ' // 从视频第1秒处截取画面
'-vframes 1 ' // 只截取一帧画面
'-vf scale=128:-1 ' // 设置缩略图宽度为128像素,高度按比例缩放
'-y ' // 覆盖已存在的输出文件
'"$thumbnailPath"'; // 指定输出文件路径
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
if (ReturnCode.isSuccess(returnCode)) {
return thumbnailPath;
} else {
print('生成视频缩略图失败: ${await session.getFailStackTrace()}');
return null;
}
} catch (e) { } catch (e) {
print('生成视频缩略图出错: $e'); print('生成视频缩略图出错: $e');
return null; return null;
} }
// try {
// final thumbnailPath = '${dir.path}/video_thumb_${DateTime.now().millisecondsSinceEpoch}.jpg';
//
// final thumbPath = await VideoThumbnail.thumbnailFile(
// video: videoPath,
// thumbnailPath: thumbnailPath,
// imageFormat: ImageFormat.JPEG,
// maxWidth: 128, // 缩略图最大宽度
// quality: 75, // 图片质量
// );
//
// return thumbPath;
// } catch (e) {
// print('生成视频缩略图出错: $e');
// return null;
// }
} }
} }
...@@ -9,25 +9,25 @@ class VideoUtil { ...@@ -9,25 +9,25 @@ class VideoUtil {
/// 转码的同时,进行压缩 /// 转码的同时,进行压缩
/// ///
static Future<bool> convertToMp4(String inputPath, String outputPath) async { static Future<bool> convertToMp4(String inputPath, String outputPath) async {
String cmd; String cmd;
if(Platform.isIOS) { if (Platform.isIOS) {
// 构建命令 cmd = '-i "$inputPath" '
// 1. -c:v h264_videotoolbox : 启用 iOS 硬件加速 '-c:v h264_videotoolbox ' // 启用 iOS 硬件加速
// 2. -b:v 1500k : 限制视频码率为 1.5Mbps (体积小,手机看足够) '-b:v 1500k ' // 限制视频码率为 1.5Mbps (体积小,手机看足够)
// 3. -vf scale=1280:-2 : 缩放到 720p (保持比例) '-vf scale=1280:-2 ' // 缩放到 720p (保持比例)
// 4. -c:a aac : 音频转为 AAC (兼容性最好) '-c:a aac ' // 音频转为 AAC (兼容性最好)
// 5. -b:a 128k : 音频码率 '-b:a 128k ' // 音频码率
cmd =
'-i "$inputPath" '
'-c:v h264_videotoolbox -b:v 1500k '
'-vf scale=1280:-2 '
'-c:a aac -b:a 128k '
'"$outputPath"'; '"$outputPath"';
print("开始极速转码: $cmd");
} else { } else {
cmd = '-i "$inputPath" -c:v libx264 -crf 28 -c:a aac -b:a 128k -strict experimental -movflags faststart -f mp4 "$outputPath"'; cmd = '-i "$inputPath" ' // 指定输入文件路径
'-c:v libx264 ' // 设置视频编码器为libx264(H.264)
'-crf 28 ' // 设置恒定速率因子CRF为28(中等压缩质量)
'-c:a aac ' // 设置音频编码器为AAC
'-b:a 128k ' // 设置音频比特率为128kbps
'-strict experimental ' // 允许使用实验性编解码器功能
'-movflags faststart ' // 优化MP4文件结构,使视频可以快速启动播放
'-f mp4 ' // 指定输出格式为MP4
'"$outputPath"'; // 指定输出文件路径
} }
final session = await FFmpegKit.execute(cmd); final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
...@@ -56,8 +56,15 @@ class VideoUtil { ...@@ -56,8 +56,15 @@ class VideoUtil {
default: default:
throw Exception('参数错误'); throw Exception('参数错误');
} }
final session = await FFmpegKit.execute( String cmd = '-i "$inputPath" ' // 输入文件
'-i "$inputPath" -c:v libx264 -crf $crf -c:a aac -b:a 128k -preset medium -movflags faststart "$outputPath"'); '-c:v libx264 ' // 视频编码器
'-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率
'-preset medium ' // 编码预设
'-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode); return ReturnCode.isSuccess(returnCode);
} }
......
...@@ -37,8 +37,7 @@ dependencies: ...@@ -37,8 +37,7 @@ dependencies:
flutter_bloc: ^9.1.1 flutter_bloc: ^9.1.1
flutter_localization: ^0.3.3 flutter_localization: ^0.3.3
flutter_image_compress: ^2.4.0 flutter_image_compress: ^2.4.0
ffmpeg_kit_flutter_new: ^4.1.0
# 建议:如果项目中不需要 image_picker(被 wechat_assets_picker 替代),则保持注释或删除 # 建议:如果项目中不需要 image_picker(被 wechat_assets_picker 替代),则保持注释或删除
# image_picker: ^1.2.0 # image_picker: ^1.2.0
...@@ -55,8 +54,8 @@ dependencies: ...@@ -55,8 +54,8 @@ dependencies:
# --- 音视频与直播 (重灾区) --- # --- 音视频与直播 (重灾区) ---
# 确保 ffmpeg_kit 版本与你的架构兼容。 # 确保 ffmpeg_kit 版本与你的架构兼容。
# 如果只是为了压缩视频,建议评估是否移除 video_compress,直接用 ffmpeg # 如果只是为了压缩视频,建议评估是否移除 video_compress,直接用 ffmpeg
#ffmpeg_kit_flutter_new: ^3.2.0 ffmpeg_kit_flutter_new: ^4.1.0
video_compress: ^3.1.4 # video_compress: ^3.1.4
video_player: ^2.10.0 video_player: ^2.10.0
# video_thumbnail 已被注释,确认是否需要生成缩略图,如果需要,ffmpeg_kit 也能做 # video_thumbnail 已被注释,确认是否需要生成缩略图,如果需要,ffmpeg_kit 也能做
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!