Commit 552c2145 by tanghuan

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

1 parent 596a373b
......@@ -408,6 +408,67 @@ class WebCubit extends Cubit<WebState> with WidgetsBindingObserver {
_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() {
emit(state.copyWith(loaded: true));
}
......
......@@ -24,7 +24,7 @@ class Constant {
/// /////////////////////////////////////////////////////////////////////////////////////////////////////////////////
///
/// obs文件分片上传的分片大小:5M
static const int obsUploadChunkSize = 1024 * 1024 * 5;
static const int obsUploadChunkSize = 1024 * 1024 * 1;
/// obs文件上传的逻辑前缀
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';
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/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/video_info_handler.dart';
import 'package:appframe/data/repositories/message/wifi_info_handler.dart';
......@@ -179,6 +180,7 @@ Future<void> setupLocator() async {
/// 上传文件
getIt.registerLazySingleton<MessageHandler>(() => UploadFileHandler(), instanceName: 'uploadFile');
getIt.registerLazySingleton<MessageHandler>(() => UploadStartHandler(), instanceName: 'uploadStart');
/// 下载文件
getIt.registerLazySingleton<MessageHandler>(() => DownloadFileHandler(), instanceName: 'downloadFile');
......
......@@ -4,6 +4,7 @@ import 'package:appframe/bloc/web_cubit.dart';
import 'package:appframe/config/locator.dart';
import 'package:appframe/data/models/message/h5_message.dart';
import 'package:appframe/data/models/message/h5_resp.dart';
import 'package:appframe/data/repositories/message/upload_start_handler.dart';
// 消息处理器抽象类
abstract class MessageHandler {
......@@ -56,11 +57,17 @@ class MessageDispatcher {
h5Message.cmd == "goLogin" ||
h5Message.cmd.startsWith("setTitlebar") ||
h5Message.cmd == "audioPlay" ||
h5Message.cmd == "openLink") {
h5Message.cmd == "openLink" ||
h5Message.cmd == "uploadStart") {
handler.setCubit(webCubit!);
handler.setMessage(message);
}
// 针对 uploadStart 指令
if (h5Message.cmd == "uploadStart" && handler is UploadStartHandler) {
handler.setCmdUnique(h5Message.unique);
}
final result = await handler.handleMessage(h5Message.params);
// 有些命令需要通过监听器调用Cubit,触发调用时不需要返回结果,不处理回调
if (result == null) {
......
import 'dart:io';
import 'dart:async';
import 'package:ffmpeg_kit_flutter_new/ffmpeg_kit.dart';
import 'package:ffmpeg_kit_flutter_new/return_code.dart';
import 'package:ffmpeg_kit_flutter_new/statistics.dart';
class VideoUtil {
///
/// 将视频格式转换为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;
if (Platform.isIOS) {
cmd = '-i "$inputPath" '
......@@ -24,20 +34,47 @@ class VideoUtil {
'-crf 28 ' // 设置恒定速率因子CRF为28(中等压缩质量)
'-c:a aac ' // 设置音频编码器为AAC
'-b:a 128k ' // 设置音频比特率为128kbps
'-preset fast '
'-threads 0 '
'-strict experimental ' // 允许使用实验性编解码器功能
'-movflags faststart ' // 优化MP4文件结构,使视频可以快速启动播放
'-f mp4 ' // 指定输出格式为MP4
'"$outputPath"'; // 指定输出文件路径
}
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode);
final completer = Completer<bool>();
FFmpegKit.executeAsync(
cmd,
(session) async {
final returnCode = await session.getReturnCode();
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 压缩视频
/// [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 18-20
// 中等质量: CRF 23-26
......@@ -61,12 +98,50 @@ class VideoUtil {
'-crf $crf ' // 恒定速率因子(质量控制)
'-c:a aac ' // 音频编码器
'-b:a 128k ' // 音频比特率
'-preset medium ' // 编码预设
// '-preset medium ' // 编码预设
'-preset fast ' // 编码预设,编码速度显著提升,体积损失很小
'-threads 0 ' // 让 libx264 自动使用所有可用 CPU 核心,默认行为可能只用单核
'-movflags faststart ' // 优化MP4文件结构
'"$outputPath"'; // 输出文件
final session = await FFmpegKit.execute(cmd);
final returnCode = await session.getReturnCode();
return ReturnCode.isSuccess(returnCode);
final completer = Completer<bool>();
FFmpegKit.executeAsync(
cmd,
(session) async {
final returnCode = await session.getReturnCode();
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!