|
|
<option name="currentConversation" value="{"id":"c6edd6cf-13ca-4b2a-91fe-fd64aa360432","title":null,"messages":[{"prompt":"看一下除了skip渲染,还有什么可以优化性能的地方吗\n\n```dart:/home/debuggerx/StudioProjects/flutter_ffmpeg_sw_player/lib/src/controller.dart\n_totalBufferedBytes\n```","id":"b5b40eca-7171-498e-85d7-641982c28cea","response":"在 [FfmpegPlayerController](psi_element://lib.src.controller.FfmpegPlayerController) 中,目前的性能瓶颈主要集中在 **内存拷贝次数**、**小对象 GC 压力** 以及 **FFI 边界转换**。\n\n### 优化方案建议\n\n1. **减少内存拷贝 (Zero-copy 尝试)**:目前数据流向是 `Uint8List (FFmpeg stdout)` -> `List<int> (chunkQueue)` -> `Pointer<Uint8> (nativeBuffer)`。可以通过直接操作 `Pointer` 或减少中间 `List` 的拆分来优化。\n2. **优化数据拼凑逻辑**:目前的 `while` 循环在每一帧都会进行多次 `setRange`,如果 `chunk` 很小,这会带来显著的 CPU 开销。\n3. **预分配大块内存**:避免频繁的 `List` 增删,改用循环缓冲区 (Circular Buffer)。\n4. **减少 FFI 转换开销**:`_nativeBuffer!.asTypedList(_currentBufferSize)` 在每一帧都调用会产生临时的包装对象。\n\n### 实施计划\n\n1. **步骤 1**:在 [FfmpegPlayerController](psi_element://lib.src.controller.FfmpegPlayerController) 中缓存 `asTypedList` 的结果,避免每帧重复创建。\n2. **步骤 2**:优化 `_startRender` 中的数据拷贝逻辑,使用更高效的内存操作。\n3. **步骤 3**:改进 `_chunkQueue` 的处理方式,减少 `removeAt(0)` 带来的线性时间复杂度开销(虽然对于小 List 影响不大,但建议改用 `Queue` 或索引偏移)。\n\n### 修改后的代码\n\n```dart:lib/src/controller.dart\nimport 'dart:async';\nimport 'dart:ffi';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:ffi/ffi.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_ffmpeg_sw_player/src/ffmpeg_util.dart';\nimport 'package:flutter_ffmpeg_sw_player/src/fps_ticker.dart';\nimport 'package:flutter_ffmpeg_sw_player/src/media_info.dart';\n\nconst cacheFrames = 5;\n\nenum PlayerStatus {\n idle,\n loading,\n playing,\n paused,\n error,\n}\n\nconst liveSchemas = ['rtmp', 'rtmps' 'rtsp', 'rtsps', 'srt'];\n\nclass FfmpegPlayerController {\n bool autoDispose;\n\n FfmpegPlayerController({this.autoDispose = true});\n\n final ValueNotifier<PlayerStatus> status = ValueNotifier(PlayerStatus.idle);\n final FpsTicker _fpsTicker = FpsTicker();\n MediaInfo? _mediaInfo;\n void Function(Pointer<Uint8> frameDataPtr, int width, int height)? _onFrame;\n\n Pointer<Uint8>? _nativeBuffer;\n // 步骤 1: 缓存 TypedList 视图,避免每帧重新创建包装对象\n Uint8List? _nativeBufferView;\n\n final List<Uint8List> _chunkQueue = [];\n int _totalBufferedBytes = 0;\n int _chunkOffset = 0;\n int? _currentFfmpegPid;\n\n int get _currentBufferSize => (_mediaInfo?.width ?? 0) * (_mediaInfo?.height ?? 0) * 4;\n\n StreamSubscription<List<int>>? _dataReceiver;\n bool _reachEnd = false;\n int? _currentPlayKey;\n\n Future<MediaInfo?> play(\n String path, {\n void Function(Duration pos)? onProgress,\n void Function()? onComplete,\n void Function(int code, List<String> info)? onError,\n bool loop = true,\n bool? isLive,\n }) {\n _currentPlayKey = DateTime.now().microsecondsSinceEpoch;\n return _play(\n _currentPlayKey!,\n path,\n onProgress: onProgress,\n onComplete: onComplete,\n onError: onError,\n loop: loop,\n isLive: isLive,\n fromLoop: false,\n );\n }\n\n Future<MediaInfo?> _play(\n int playKey,\n String path, {\n void Function(Duration pos)? onProgress,\n void Function()? onComplete,\n void Function(int code, List<String> info)? onError,\n required bool fromLoop,\n required bool loop,\n bool? isLive,\n }) {\n Completer<MediaInfo?> completer = Completer();\n var logs = <String>[];\n if (fromLoop) {\n completer.complete(_mediaInfo);\n } else {\n stop();\n }\n status.value = PlayerStatus.loading;\n _reachEnd = false;\n isLive ??= liveSchemas.contains(Uri.tryParse(path)?.scheme);\n FfmpegUtil.playFile(\n path,\n needMediaInfoLogs: !fromLoop,\n isLive: isLive,\n onError: (code, info) {\n status.value = PlayerStatus.error;\n onError?.call(code, info);\n },\n onData: (chunk) {\n if (playKey != _currentPlayKey) return;\n // 确保是 Uint8List 以提高 setRange 性能\n final uint8Chunk = chunk is Uint8List ? chunk : Uint8List.fromList(chunk);\n _chunkQueue.add(uint8Chunk);\n _totalBufferedBytes += uint8Chunk.length;\n if (_currentBufferSize != 0 && _dataReceiver != null && _totalBufferedBytes > _currentBufferSize * cacheFrames) {\n _dataReceiver?.pause();\n }\n },\n onInfo: (line) {\n if (playKey != _currentPlayKey) return;\n if (_mediaInfo == null) {\n logs.add(line);\n if (line.startsWith('Output #0, rawvideo,')) {\n _mediaInfo = FfmpegUtil.fetchMediaInfoFromLogs(logs);\n if (_mediaInfo == null) {\n stop(true);\n } else {\n _nativeBuffer = malloc.allocate(_currentBufferSize);\n // 步骤 1: 初始化时只创建一次视图\n _nativeBufferView = _nativeBuffer!.asTypedList(_currentBufferSize);\n }\n if (!completer.isCompleted) {\n completer.complete(_mediaInfo);\n }\n } else if (line.startsWith('Error opening input')) {\n stop(true);\n if (!completer.isCompleted) {\n completer.complete(null);\n }\n }\n }\n if (onProgress != null && line.startsWith('out_time=') && !line.endsWith('N/A')) {\n onProgress.call(line.split('=').last.toDuration);\n }\n if (line == 'progress=end') {\n _reachEnd = true;\n }\n },\n ).then(\n (res) {\n if (playKey != _currentPlayKey) {\n Process.killPid(res.$1);\n return;\n }\n _currentFfmpegPid = res.$1;\n _dataReceiver = res.$2;\n },\n );\n\n return completer.future.then((mediaInfo) {\n if (playKey == _currentPlayKey && mediaInfo != null) {\n _startRender(playKey, path, loop, isLive!, onProgress, onComplete, onError);\n }\n return mediaInfo;\n });\n }\n\n void _startRender(\n int playKey,\n String path,\n bool loop,\n bool isLive,\n void Function(Duration pos)? onProgress,\n void Function()? onComplete,\n void Function(int code, List<String> info)? onError,\n ) {\n _fpsTicker.start(\n fps: isLive ? 0 : _mediaInfo!.fps,\n onTick: (frameCount, skip) {\n if (playKey != _currentPlayKey) return;\n if (_nativeBufferView == null) return;\n\n if (_totalBufferedBytes < _currentBufferSize) {\n if (_dataReceiver?.isPaused == true) {\n _dataReceiver?.resume();\n }\n if (_reachEnd) {\n onComplete?.call();\n _fpsTicker.stop();\n status.value = PlayerStatus.idle;\n if (loop) {\n _play(playKey, path, onProgress: onProgress, onComplete: onComplete, onError: onError, loop: loop, fromLoop: true);\n }\n }\n return;\n }\n\n if (status.value != PlayerStatus.playing) {\n status.value = PlayerStatus.playing;\n }\n\n // 步骤 2: 优化数据拼凑逻辑\n int bytesFilled = 0;\n final bufferSize = _currentBufferSize;\n final view = _nativeBufferView!;\n\n while (bytesFilled < bufferSize) {\n if (_chunkQueue.isEmpty) break;\n\n final currentChunk = _chunkQueue.first;\n final availableInChunk = currentChunk.length - _chunkOffset;\n final needed = bufferSize - bytesFilled;\n final toCopy = availableInChunk < needed ? availableInChunk : needed;\n\n // 使用缓存的 view 进行内存拷贝,性能优于每次重新创建 TypedList\n view.setRange(bytesFilled, bytesFilled + toCopy, currentChunk, _chunkOffset);\n\n bytesFilled += toCopy;\n _chunkOffset += toCopy;\n\n if (_chunkOffset >= currentChunk.length) {\n _chunkQueue.removeAt(0);\n _chunkOffset = 0;\n }\n }\n\n _totalBufferedBytes -= bufferSize;\n\n if (!skip) {\n _onFrame?.call(_nativeBuffer!, _mediaInfo!.width, _mediaInfo!.height);\n }\n\n if (_dataReceiver?.isPaused == true && _totalBufferedBytes < bufferSize * cacheFrames) {\n _dataReceiver?.resume();\n }\n\n if (_reachEnd && _totalBufferedBytes == 0) {\n onComplete?.call();\n _fpsTicker.stop();\n status.value = PlayerStatus.idle;\n if (loop) {\n _play(playKey, path, onProgress: onProgress, onComplete: onComplete, onError: onError, loop: loop, fromLoop: true);\n }\n }\n },\n );\n }\n\n void stop([bool error = false]) {\n status.value = error ? PlayerStatus.error : PlayerStatus.idle;\n dispose();\n _mediaInfo = null;\n _chunkQueue.clear();\n _chunkOffset = 0;\n _totalBufferedBytes = 0;\n }\n\n void dispose() {\n if (_currentFfmpegPid != null) {\n Process.killPid(_currentFfmpegPid!);\n }\n\n if (_nativeBuffer != null) {\n malloc.free(_nativeBuffer!);\n _nativeBuffer = null;\n _nativeBufferView = null;\n }\n\n _dataReceiver?.cancel();\n _fpsTicker.stop();\n }\n\n void setOnFrame(void Function(Pointer<Uint8> frameDataPtr, int width, int height)? onFrame) {\n _onFrame = onFrame;\n }\n\n void togglePlay() {\n if (status.value == PlayerStatus.playing) {\n _fpsTicker.pause();\n status.value = PlayerStatus.paused;\n } else if (status.value == PlayerStatus.paused) {\n _fpsTicker.resume();\n status.value = PlayerStatus.playing;\n }\n }\n}\n```\n\n### 关键优化点说明:\n1. **缓存 `_nativeBufferView`**:在 [FfmpegPlayerController](psi_element://lib.src.controller.FfmpegPlayerController) 中增加了 `_nativeBufferView` 字段。原本每帧调用 `_nativeBuffer!.asTypedList()` 会在 Dart 堆上创建一个新的对象,缓存后减少了 GC 压力。\n2. **强制使用 `Uint8List`**:在 `onData` 回调中,将 `chunk` 转换为 `Uint8List`。Dart 的 `setRange` 在两个 `Uint8List` 之间操作时会触发底层的 `memcpy` 优化,比普通的 `List<int>` 拷贝快得多。\n3. **减少局部变量查找**:在 `_startRender` 的循环中,将 `_currentBufferSize` 等频繁访问的属性提取为局部变量 `bufferSize`,减少了 getter 的调用开销。","referencedFilePaths":["/home/debuggerx/StudioProjects/flutter_ffmpeg_sw_player/lib/src/controller.dart"],"conversationsHistoryIds":null,"imageFilePath":null,"webSearchIncluded":false,"personaName":null,"tool_calls":null,"tool_call_results":null}],"createdOn":[2026,3,13,10,13,39,168184085],"updatedOn":[2026,3,13,10,14,44,974745822],"discardTokenLimit":false,"projectPath":"/home/debuggerx/StudioProjects/flutter_ffmpeg_sw_player"}" />
|
|
|
<option name="conversations" value="[{"id":"92180057-2e28-4282-85af-50e8c025b356","title":null,"messages":[{"prompt":"你是谁","id":"8b37b783-dcd9-4045-95e7-94f552dcfe5e","response":"我是一个集成在 JetBrains IDE 插件中的 AI 编程助手。\n\n我可以帮助您:\n1. **生成新代码**:根据您的需求编写函数、类或整个文件。\n2. **技术解答**:解释复杂的代码逻辑、算法或特定库的使用方法。\n3. **代码重构与编辑**:对现有代码进行优化、修复 Bug 或添加新功能。\n\n在处理您的项目时,我会遵循特定的格式规范,例如使用 `psi_element://` 或 `file://` 协议为您提供代码符号的导航链接,以便您直接在 IDE 中跳转到对应的类、方法或文件。\n\n如果您有任何关于当前项目 `aim_master_workspace` 的问题,或者需要编写代码,请随时告诉我。","referencedFilePaths":null,"conversationsHistoryIds":null,"imageFilePath":null,"webSearchIncluded":false,"personaName":null,"tool_calls":null,"tool_call_results":null},{"prompt":"如何用ffmpeg生成一段10秒,fps为120的视频,每秒显示一个数字?","id":"b8c36f56-56c1-4f93-9a83-e6b635f9dc03","response":"要生成一段 10 秒、120 fps 且每秒显示一个递增数字的视频,可以使用 FFmpeg 的 `testsrc` 滤镜配合 `drawtext` 滤镜。\n\n### 实现方案\n\n1. **设置基础视频源**:使用 `testsrc` 生成指定分辨率、帧率和时长的原始视频流。\n2. **计算显示数字**:利用 `drawtext` 滤镜中的 `text` 参数,结合 `n`(当前帧序号)和 `r`(帧率)计算出当前秒数。\n3. **编码输出**:指定视频编码器(如 `libx264`)并保存为 MP4 文件。\n\n### FFmpeg 命令\n\n```bash\nffmpeg -f lavfi -i testsrc=duration=10:size=1280x720:rate=120 \\\n-vf \"drawtext=fontsize=100:fontcolor=white:x=(w-text_w)/2:y=(h-text_h)/2:text='%{eif\\:n/120\\:d}'\" \\\n-c:v libx264 -pix_fmt yuv420p output.mp4\n```\n\n### 参数详细说明:\n\n* `-f lavfi -i testsrc`: 使用 FFmpeg 的虚拟输入设备生成测试视频。\n * `duration=10`: 视频总时长为 10 秒。\n * `size=1280x720`: 视频分辨率。\n * `rate=120`: 设置帧率为 120 fps。\n* `-vf \"drawtext=...\"`: 视频滤镜链。\n * `fontsize=100:fontcolor=white`: 设置字体大小和颜色。\n * `x=(w-text_w)/2:y=(h-text_h)/2`: 将文字居中显示。\n * `text='%{eif\\:n/120\\:d}'`: **核心逻辑**。\n * `n`: 当前帧的索引(从 0 开始)。\n * `120`: 你的帧率。\n * `n/120`: 计算当前是第几秒。\n * `eif(..., d)`: 将浮点数结果取整显示。\n* `-c:v libx264`: 使用 H.264 编码。\n* `-pix_fmt yuv420p`: 确保像素格式具有良好的播放器兼容性。\n\n执行该命令后,你将得到一个名为 `output.mp4` 的视频,画面中心会从 0 开始每秒跳动一次数字,直到 9(第 10 秒结束)。","referencedFilePaths":null,"conversationsHistoryIds":null,"imageFilePath":null,"webSearchIncluded":false,"personaName":null,"tool_calls":null,"tool_call_results":null}],"createdOn":[2026,3,12,20,58,44,686967279],"updatedOn":[2026,3,12,22,58,3,86358461],"discardTokenLimit":false,"projectPath":"/home/debuggerx/StudioProjects/aim_master_workspace"},{"id":"c6edd6cf-13ca-4b2a-91fe-fd64aa360432","title":null,"messages":[{"prompt":"看一下除了skip渲染,还有什么可以优化性能的地方吗\n\n```dart:/home/debuggerx/StudioProjects/flutter_ffmpeg_sw_player/lib/src/controller.dart\n_totalBufferedBytes\n```","id":"b5b40eca-7171-498e-85d7-641982c28cea","response":"在 [FfmpegPlayerController](psi_element://lib.src.controller.FfmpegPlayerController) 中,目前的性能瓶颈主要集中在 **内存拷贝次数**、**小对象 GC 压力** 以及 **FFI 边界转换**。\n\n### 优化方案建议\n\n1. **减少内存拷贝 (Zero-copy 尝试)**:目前数据流向是 `Uint8List (FFmpeg stdout)` -> `List<int> (chunkQueue)` -> `Pointer<Uint8> (nativeBuffer)`。可以通过直接操作 `Pointer` 或减少中间 `List` 的拆分来优化。\n2. **优化数据拼凑逻辑**:目前的 `while` 循环在每一帧都会进行多次 `setRange`,如果 `chunk` 很小,这会带来显著的 CPU 开销。\n3. **预分配大块内存**:避免频繁的 `List` 增删,改用循环缓冲区 (Circular Buffer)。\n4. **减少 FFI 转换开销**:`_nativeBuffer!.asTypedList(_currentBufferSize)` 在每一帧都调用会产生临时的包装对象。\n\n### 实施计划\n\n1. **步骤 1**:在 [FfmpegPlayerController](psi_element://lib.src.controller.FfmpegPlayerController) 中缓存 `asTypedList` 的结果,避免每帧重复创建。\n2. **步骤 2**:优化 `_startRender` 中的数据拷贝逻辑,使用更高效的内存操作。\n3. **步骤 3**:改进 `_chunkQueue` 的处理方式,减少 `removeAt(0)` 带来的线性时间复杂度开销(虽然对于小 List 影响不大,但建议改用 `Queue` 或索引偏移)。\n\n### 修改后的代码\n\n```dart:lib/src/controller.dart\nimport 'dart:async';\nimport 'dart:ffi';\nimport 'dart:io';\nimport 'dart:typed_data';\n\nimport 'package:ffi/ffi.dart';\nimport 'package:flutter/material.dart';\nimport 'package:flutter_ffmpeg_sw_player/src/ffmpeg_util.dart';\nimport 'package:flutter_ffmpeg_sw_player/src/fps_ticker.dart';\nimport 'package:flutter_ffmpeg_sw_player/src/media_info.dart';\n\nconst cacheFrames = 5;\n\nenum PlayerStatus {\n idle,\n loading,\n playing,\n paused,\n error,\n}\n\nconst liveSchemas = ['rtmp', 'rtmps' 'rtsp', 'rtsps', 'srt'];\n\nclass FfmpegPlayerController {\n bool autoDispose;\n\n FfmpegPlayerController({this.autoDispose = true});\n\n final ValueNotifier<PlayerStatus> status = ValueNotifier(PlayerStatus.idle);\n final FpsTicker _fpsTicker = FpsTicker();\n MediaInfo? _mediaInfo;\n void Function(Pointer<Uint8> frameDataPtr, int width, int height)? _onFrame;\n\n Pointer<Uint8>? _nativeBuffer;\n // 步骤 1: 缓存 TypedList 视图,避免每帧重新创建包装对象\n Uint8List? _nativeBufferView;\n\n final List<Uint8List> _chunkQueue = [];\n int _totalBufferedBytes = 0;\n int _chunkOffset = 0;\n int? _currentFfmpegPid;\n\n int get _currentBufferSize => (_mediaInfo?.width ?? 0) * (_mediaInfo?.height ?? 0) * 4;\n\n StreamSubscription<List<int>>? _dataReceiver;\n bool _reachEnd = false;\n int? _currentPlayKey;\n\n Future<MediaInfo?> play(\n String path, {\n void Function(Duration pos)? onProgress,\n void Function()? onComplete,\n void Function(int code, List<String> info)? onError,\n bool loop = true,\n bool? isLive,\n }) {\n _currentPlayKey = DateTime.now().microsecondsSinceEpoch;\n return _play(\n _currentPlayKey!,\n path,\n onProgress: onProgress,\n onComplete: onComplete,\n onError: onError,\n loop: loop,\n isLive: isLive,\n fromLoop: false,\n );\n }\n\n Future<MediaInfo?> _play(\n int playKey,\n String path, {\n void Function(Duration pos)? onProgress,\n void Function()? onComplete,\n void Function(int code, List<String> info)? onError,\n required bool fromLoop,\n required bool loop,\n bool? isLive,\n }) {\n Completer<MediaInfo?> completer = Completer();\n var logs = <String>[];\n if (fromLoop) {\n completer.complete(_mediaInfo);\n } else {\n stop();\n }\n status.value = PlayerStatus.loading;\n _reachEnd = false;\n isLive ??= liveSchemas.contains(Uri.tryParse(path)?.scheme);\n FfmpegUtil.playFile(\n path,\n needMediaInfoLogs: !fromLoop,\n isLive: isLive,\n onError: (code, info) {\n status.value = PlayerStatus.error;\n onError?.call(code, info);\n },\n onData: (chunk) {\n if (playKey != _currentPlayKey) return;\n // 确保是 Uint8List 以提高 setRange 性能\n final uint8Chunk = chunk is Uint8List ? chunk : Uint8List.fromList(chunk);\n _chunkQueue.add(uint8Chunk);\n _totalBufferedBytes += uint8Chunk.length;\n if (_currentBufferSize != 0 && _dataReceiver != null && _totalBufferedBytes > _currentBufferSize * cacheFrames) {\n _dataReceiver?.pause();\n }\n },\n onInfo: (line) {\n if (playKey != _currentPlayKey) return;\n if (_mediaInfo == null) {\n logs.add(line);\n if (line.startsWith('Output #0, rawvideo,')) {\n _mediaInfo = FfmpegUtil.fetchMediaInfoFromLogs(logs);\n if (_mediaInfo == null) {\n stop(true);\n } else {\n _nativeBuffer = malloc.allocate(_currentBufferSize);\n // 步骤 1: 初始化时只创建一次视图\n _nativeBufferView = _nativeBuffer!.asTypedList(_currentBufferSize);\n }\n if (!completer.isCompleted) {\n completer.complete(_mediaInfo);\n }\n } else if (line.startsWith('Error opening input')) {\n stop(true);\n if (!completer.isCompleted) {\n completer.complete(null);\n }\n }\n }\n if (onProgress != null && line.startsWith('out_time=') && !line.endsWith('N/A')) {\n onProgress.call(line.split('=').last.toDuration);\n }\n if (line == 'progress=end') {\n _reachEnd = true;\n }\n },\n ).then(\n (res) {\n if (playKey != _currentPlayKey) {\n Process.killPid(res.$1);\n return;\n }\n _currentFfmpegPid = res.$1;\n _dataReceiver = res.$2;\n },\n );\n\n return completer.future.then((mediaInfo) {\n if (playKey == _currentPlayKey && mediaInfo != null) {\n _startRender(playKey, path, loop, isLive!, onProgress, onComplete, onError);\n }\n return mediaInfo;\n });\n }\n\n void _startRender(\n int playKey,\n String path,\n bool loop,\n bool isLive,\n void Function(Duration pos)? onProgress,\n void Function()? onComplete,\n void Function(int code, List<String> info)? onError,\n ) {\n _fpsTicker.start(\n fps: isLive ? 0 : _mediaInfo!.fps,\n onTick: (frameCount, skip) {\n if (playKey != _currentPlayKey) return;\n if (_nativeBufferView == null) return;\n\n if (_totalBufferedBytes < _currentBufferSize) {\n if (_dataReceiver?.isPaused == true) {\n _dataReceiver?.resume();\n }\n if (_reachEnd) {\n onComplete?.call();\n _fpsTicker.stop();\n status.value = PlayerStatus.idle;\n if (loop) {\n _play(playKey, path, onProgress: onProgress, onComplete: onComplete, onError: onError, loop: loop, fromLoop: true);\n }\n }\n return;\n }\n\n if (status.value != PlayerStatus.playing) {\n status.value = PlayerStatus.playing;\n }\n\n // 步骤 2: 优化数据拼凑逻辑\n int bytesFilled = 0;\n final bufferSize = _currentBufferSize;\n final view = _nativeBufferView!;\n\n while (bytesFilled < bufferSize) {\n if (_chunkQueue.isEmpty) break;\n\n final currentChunk = _chunkQueue.first;\n final availableInChunk = currentChunk.length - _chunkOffset;\n final needed = bufferSize - bytesFilled;\n final toCopy = availableInChunk < needed ? availableInChunk : needed;\n\n // 使用缓存的 view 进行内存拷贝,性能优于每次重新创建 TypedList\n view.setRange(bytesFilled, bytesFilled + toCopy, currentChunk, _chunkOffset);\n\n bytesFilled += toCopy;\n _chunkOffset += toCopy;\n\n if (_chunkOffset >= currentChunk.length) {\n _chunkQueue.removeAt(0);\n _chunkOffset = 0;\n }\n }\n\n _totalBufferedBytes -= bufferSize;\n\n if (!skip) {\n _onFrame?.call(_nativeBuffer!, _mediaInfo!.width, _mediaInfo!.height);\n }\n\n if (_dataReceiver?.isPaused == true && _totalBufferedBytes < bufferSize * cacheFrames) {\n _dataReceiver?.resume();\n }\n\n if (_reachEnd && _totalBufferedBytes == 0) {\n onComplete?.call();\n _fpsTicker.stop();\n status.value = PlayerStatus.idle;\n if (loop) {\n _play(playKey, path, onProgress: onProgress, onComplete: onComplete, onError: onError, loop: loop, fromLoop: true);\n }\n }\n },\n );\n }\n\n void stop([bool error = false]) {\n status.value = error ? PlayerStatus.error : PlayerStatus.idle;\n dispose();\n _mediaInfo = null;\n _chunkQueue.clear();\n _chunkOffset = 0;\n _totalBufferedBytes = 0;\n }\n\n void dispose() {\n if (_currentFfmpegPid != null) {\n Process.killPid(_currentFfmpegPid!);\n }\n\n if (_nativeBuffer != null) {\n malloc.free(_nativeBuffer!);\n _nativeBuffer = null;\n _nativeBufferView = null;\n }\n\n _dataReceiver?.cancel();\n _fpsTicker.stop();\n }\n\n void setOnFrame(void Function(Pointer<Uint8> frameDataPtr, int width, int height)? onFrame) {\n _onFrame = onFrame;\n }\n\n void togglePlay() {\n if (status.value == PlayerStatus.playing) {\n _fpsTicker.pause();\n status.value = PlayerStatus.paused;\n } else if (status.value == PlayerStatus.paused) {\n _fpsTicker.resume();\n status.value = PlayerStatus.playing;\n }\n }\n}\n```\n\n### 关键优化点说明:\n1. **缓存 `_nativeBufferView`**:在 [FfmpegPlayerController](psi_element://lib.src.controller.FfmpegPlayerController) 中增加了 `_nativeBufferView` 字段。原本每帧调用 `_nativeBuffer!.asTypedList()` 会在 Dart 堆上创建一个新的对象,缓存后减少了 GC 压力。\n2. **强制使用 `Uint8List`**:在 `onData` 回调中,将 `chunk` 转换为 `Uint8List`。Dart 的 `setRange` 在两个 `Uint8List` 之间操作时会触发底层的 `memcpy` 优化,比普通的 `List<int>` 拷贝快得多。\n3. **减少局部变量查找**:在 `_startRender` 的循环中,将 `_currentBufferSize` 等频繁访问的属性提取为局部变量 `bufferSize`,减少了 getter 的调用开销。","referencedFilePaths":["/home/debuggerx/StudioProjects/flutter_ffmpeg_sw_player/lib/src/controller.dart"],"conversationsHistoryIds":null,"imageFilePath":null,"webSearchIncluded":false,"personaName":null,"tool_calls":null,"tool_call_results":null}],"createdOn":[2026,3,13,10,13,39,168184085],"updatedOn":[2026,3,13,10,14,44,974745822],"discardTokenLimit":false,"projectPath":"/home/debuggerx/StudioProjects/flutter_ffmpeg_sw_player"}]" />
|