Commit 552c2145 by tanghuan

上传文件:1.针对视频文件增加转码和压缩的进度反馈;2.增加上传进度反馈。

1 parent 596a373b
...@@ -408,6 +408,67 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver { ...@@ -408,6 +408,67 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
_controller.runJavaScript(script); _controller.runJavaScript(script);
} }
/// 相应 uploadStart 指令
/// [unique] 消息唯一标识
/// [uploadId] 文件上传ID标识
///
void sendUploadStartResponse(String unique, String uploadId) {
var resp = {
'unique': unique,
'cmd': 'uploadStart',
'data': {
'uploadId': uploadId,
'status': 1,
'percent': 0,
'totalPart': 0,
'sendedPart': 0,
'totalByte': 0,
'sendedByte': 0,
},
'errMsg': '',
};
_sendResponse(resp);
}
/// 发送上传进度到 WebView
/// [unique] 消息唯一标识
/// [uploadId] 文件上传ID标识
/// [totalPart] 总分片数
/// [sendedPart] 已上传分片数
/// [totalByte] 总字节数
/// [sendedByte] 已上传字节数
void sendUploadProgress(
String unique, String uploadId, int status, int percent, int totalPart, int sendedPart, int totalByte, int sendedByte) {
var resp = {
'unique': unique,
'cmd': 'uploadProgress',
'data': {
'uploadId': uploadId,
'status': status,
'percent': percent,
'totalPart': totalPart,
'sendedPart': sendedPart,
'totalByte': totalByte,
'sendedByte': sendedByte,
},
'errMsg': '',
};
_sendResponse(resp);
}
void sendUploadEnd(String unique, String uploadId, String url, {String errMsg = ''}) {
var resp = {
'unique': '',
'cmd': 'uploadEnd',
'data': {
'uploadId': uploadId,
'url': url,
},
'errMsg': errMsg
};
_sendResponse(resp);
}
void finishLoading() { void finishLoading() {
emit(state.copyWith(loaded: true)); emit(state.copyWith(loaded: true));
} }
......
...@@ -24,7 +24,7 @@ class Constant { ...@@ -24,7 +24,7 @@ class Constant {
/// ///////////////////////////////////////////////////////////////////////////////////////////////////////////////// /// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/// ///
/// obs文件分片上传的分片大小:5M /// obs文件分片上传的分片大小:5M
static const int obsUploadChunkSize = 1024 * 1024 * 5; static const int obsUploadChunkSize = 1024 * 1024 * 1;
/// obs文件上传的逻辑前缀 /// obs文件上传的逻辑前缀
static const String obsLogicPrefix = EnvConfig.env == 'dev' ? 'd2/pridel/user/' : 'p2/unpridel/user/'; static const String obsLogicPrefix = EnvConfig.env == 'dev' ? 'd2/pridel/user/' : 'p2/unpridel/user/';
......
...@@ -30,6 +30,7 @@ import 'package:appframe/data/repositories/message/share_to_wx_handler.dart'; ...@@ -30,6 +30,7 @@ import 'package:appframe/data/repositories/message/share_to_wx_handler.dart';
import 'package:appframe/data/repositories/message/storage_handler.dart'; import 'package:appframe/data/repositories/message/storage_handler.dart';
import 'package:appframe/data/repositories/message/title_bar_handler.dart'; import 'package:appframe/data/repositories/message/title_bar_handler.dart';
import 'package:appframe/data/repositories/message/upload_file.dart'; import 'package:appframe/data/repositories/message/upload_file.dart';
import 'package:appframe/data/repositories/message/upload_start_handler.dart';
import 'package:appframe/data/repositories/message/vibrate_short_handler.dart'; import 'package:appframe/data/repositories/message/vibrate_short_handler.dart';
import 'package:appframe/data/repositories/message/video_info_handler.dart'; import 'package:appframe/data/repositories/message/video_info_handler.dart';
import 'package:appframe/data/repositories/message/wifi_info_handler.dart'; import 'package:appframe/data/repositories/message/wifi_info_handler.dart';
...@@ -179,6 +180,7 @@ Future<void> setupLocator() async { ...@@ -179,6 +180,7 @@ Future<void> setupLocator() async {
/// 上传文件 /// 上传文件
getIt.registerLazySingleton<MessageHandler>(() => UploadFileHandler(), instanceName: 'uploadFile'); getIt.registerLazySingleton<MessageHandler>(() => UploadFileHandler(), instanceName: 'uploadFile');
getIt.registerLazySingleton<MessageHandler>(() => UploadStartHandler(), instanceName: 'uploadStart');
/// 下载文件 /// 下载文件
getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile'); getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile');
......
...@@ -4,6 +4,7 @@ import 'package:appframe/bloc/web_cubit.dart'; ...@@ -4,6 +4,7 @@ import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/config/locator.dart'; import 'package:appframe/config/locator.dart';
import 'package:appframe/data/models/message/h5_message.dart'; import 'package:appframe/data/models/message/h5_message.dart';
import 'package:appframe/data/models/message/h5_resp.dart'; import 'package:appframe/data/models/message/h5_resp.dart';
import 'package:appframe/data/repositories/message/upload_start_handler.dart';
// 消息处理器抽象类 // 消息处理器抽象类
abstract class MessageHandler { abstract class MessageHandler {
...@@ -56,11 +57,17 @@ class MessageDispatcher { ...@@ -56,11 +57,17 @@ class MessageDispatcher {
h5Message.cmd == "goLogin" || h5Message.cmd == "goLogin" ||
h5Message.cmd.startsWith("setTitlebar") || h5Message.cmd.startsWith("setTitlebar") ||
h5Message.cmd == "audioPlay" || h5Message.cmd == "audioPlay" ||
h5Message.cmd == "openLink") { h5Message.cmd == "openLink" ||
h5Message.cmd == "uploadStart") {
handler.setCubit(webCubit!); handler.setCubit(webCubit!);
handler.setMessage(message); handler.setMessage(message);
} }
// 针对 uploadStart 指令
if (h5Message.cmd == "uploadStart" && handler is UploadStartHandler) {
handler.setCmdUnique(h5Message.unique);
}
final result = await handler.handleMessage(h5Message.params); final result = await handler.handleMessage(h5Message.params);
// 有些命令需要通过监听器调用Cubit,触发调用时不需要返回结果,不处理回调 // 有些命令需要通过监听器调用Cubit,触发调用时不需要返回结果,不处理回调
if (result == null) { if (result == null) {
......
import 'dart:io'; import 'dart:io';
import 'dart:async';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart'; import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart'; import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:ffmpeg_kit_flutter_new/statistics.dart';
class VideoUtil { class VideoUtil {
/// ///
/// 将视频格式转换为mp4 /// 将视频格式转换为mp4
/// 转码的同时,进行压缩 /// 转码的同时,进行压缩
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
/// ///
static Future<bool> convertToMp4(String inputPath, String outputPath) async { static Future<bool> convertToMp4(
String inputPath,
String outputPath, {
void Function(int progress)? onProgress,
}) async {
// 先获取视频总时长(微秒)
final duration = await _getVideoDuration(inputPath);
String cmd; String cmd;
if (Platform.isIOS) { if (Platform.isIOS) {
cmd = '-i "$inputPath" ' cmd = '-i "$inputPath" '
...@@ -24,20 +34,47 @@ class VideoUtil { ...@@ -24,20 +34,47 @@ class VideoUtil {
'-crf 28 ' // 设置恒定速率因子CRF为28(中等压缩质量) '-crf 28 ' // 设置恒定速率因子CRF为28(中等压缩质量)
'-c:a aac ' // 设置音频编码器为AAC '-c:a aac ' // 设置音频编码器为AAC
'-b:a 128k ' // 设置音频比特率为128kbps '-b:a 128k ' // 设置音频比特率为128kbps
'-preset fast '
'-threads 0 '
'-strict experimental ' // 允许使用实验性编解码器功能 '-strict experimental ' // 允许使用实验性编解码器功能
'-movflags faststart ' // 优化MP4文件结构,使视频可以快速启动播放 '-movflags faststart ' // 优化MP4文件结构,使视频可以快速启动播放
'-f mp4 ' // 指定输出格式为MP4 '-f mp4 ' // 指定输出格式为MP4
'"$outputPath"'; // 指定输出文件路径 '"$outputPath"'; // 指定输出文件路径
} }
final session = await FFmpegKit.execute(cmd);
final completer = Completer<bool>();
FFmpegKit.executeAsync(
cmd,
(session) async {
final returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode); completer.complete(ReturnCode.isSuccess(returnCode));
},
null,
(Statistics statistics) {
if (onProgress != null && duration > 0) {
final currentTime = statistics.getTime();
final progress = (currentTime / duration).clamp(0.0, 1.0);
onProgress((progress * 100).floor());
}
},
);
return completer.future;
} }
/// ///
/// 通过 ffmpeg 压缩视频 /// 通过 ffmpeg 压缩视频
/// [onProgress] 进度回调,值范围 0.0 ~ 1.0
/// ///
static Future<bool> compressVideo(String inputPath, String outputPath, String quality) async { static Future<bool> compressVideo(
String inputPath,
String outputPath,
String quality, {
void Function(int progress)? onProgress,
}) async {
final duration = await _getVideoDuration(inputPath);
// 使用CRF模式进行压缩,值范围0-51,建议值18-28 // 使用CRF模式进行压缩,值范围0-51,建议值18-28
// 高质量: CRF 18-20 // 高质量: CRF 18-20
// 中等质量: CRF 23-26 // 中等质量: CRF 23-26
...@@ -61,12 +98,50 @@ class VideoUtil { ...@@ -61,12 +98,50 @@ class VideoUtil {
'-crf $crf ' // 恒定速率因子(质量控制) '-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器 '-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率 '-b:a 128k ' // 音频比特率
'-preset medium ' // 编码预设 // '-preset medium ' // 编码预设
'-preset fast ' // 编码预设,编码速度显著提升,体积损失很小
'-threads 0 ' // 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart ' // 优化MP4文件结构 '-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件 '"$outputPath"'; // 输出文件
final session = await FFmpegKit.execute(cmd);
final completer = Completer<bool>();
FFmpegKit.executeAsync(
cmd,
(session) async {
final returnCode = await session.getReturnCode(); final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode); completer.complete(ReturnCode.isSuccess(returnCode));
},
null,
(Statistics statistics) {
if (onProgress != null && duration > 0) {
final currentTime = statistics.getTime();
final progress = (currentTime / duration).clamp(0.0, 1.0);
onProgress((progress * 100).floor());
}
},
);
return completer.future;
}
/// 获取视频总时长,返回毫秒
static Future<int> _getVideoDuration(String videoPath) async {
final session = await FFmpegKit.execute(
'-i "$videoPath"',
);
final output = await session.getOutput();
// 从 ffmpeg 输出中解析时长,格式如: Duration: 00:01:23.45
final regex = RegExp(r'Duration:\s*(\d+):(\d+):(\d+)\.(\d+)');
final match = regex.firstMatch(output ?? '');
if (match != null) {
final hours = int.parse(match.group(1)!);
final minutes = int.parse(match.group(2)!);
final seconds = int.parse(match.group(3)!);
final ms = int.parse(match.group(4)!);
return (hours * 3600 + minutes * 60 + seconds) * 1000 + ms * 10;
}
return 0;
} }
/// 为视频文件生成缩略图 /// 为视频文件生成缩略图
......
Styling with Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!