Flutter集成imgly视频编辑SDK,Android添加下载音频时崩溃
问题:Flutter Video Editor SDK Android端添加本地下载音频崩溃
在Flutter应用中使用imgly的video_editor_sdk(版本^3.0.0),iOS端运行正常,但Android端添加从远程URL下载的音频叠加视频时崩溃。环境配置:minSdkVersion 24,compileSdkVersion 33,ext.kotlin_version = '1.7.20'。
跳转编辑器页面代码
void onEditPress() async { var filePath = Get.arguments["videoPath"]; final List<Future<String>> downloads = audioSources .map((e) => downloadAudioFile( "https://storage.googleapis.com/ps_users/imgly-background-music/$e.mp3", "$e.mp3")) .toList(); final List<String> localPaths = await Future.wait(downloads); // Convert the sources to valid `AudioClip`s. final List<AudioClip> audioClips = audioSources .asMap() .entries .map((clip) => AudioClip(clip.value, localPaths[clip.key], title: clip.value.replaceAll('_', ' ').toCapitalize().toString())) .toList(); // Create [AudioOptions] to customize the audio tool. final audioOptions = AudioOptions( categories: [AudioClipCategory("", "", items: audioClips)]); Map<String, dynamic>? serialization; // Load a serialization from the assets folder. final serializationString = sharedPreferencesRepository.getString('editChanges'); if (serializationString != null) { serialization = jsonDecode(serializationString); } // Create [SerializationOptions] to configure the serialization feature. final serializationOptions = SerializationOptions( enabled: true, // Enable the serialization feature. // For this example, the serialization should be returned as an object to simply logging it in the console. exportType: SerializationExportType.object, ); // Create [ExportOptions] to apply the [serializationOptions]. final exportOptions = ExportOptions(serialization: serializationOptions); // Create [Configuration] instance. final configuration = Configuration(export: exportOptions, audio: audioOptions); // Go to editor screen final video = Video(filePath); Get.back(); // Open the video editor and handle the export as well as any occurring errors. await VESDK.unlockWithLicense("assets/license/vesdk_license"); final result = await VESDK.openEditor(video, configuration: configuration, serialization: serialization); if (result != null) { // The user exported a new video successfully and the newly generated video is located at `result.video`. var videoPath = result.video; } else { // The user tapped on the cancel button within the editor. return; } } }
远程文件下载函数
/// Downloads the file from the [url] to the local directory. Future<String> downloadAudioFile(String url, String filename) async { final fileOutputDirectory = await getApplicationDocumentsDirectory(); String fileLocation = "${fileOutputDirectory.path}/$filename"; bool fileExist = _checkFileExistsSync(fileLocation); if (fileExist) { return fileLocation; } else { final client = HttpClient(); final request = await client.getUrl(Uri.parse(url)); final response = await request.close(); final bytes = await consolidateHttpClientResponseBytes(response); final outputFile = File(fileLocation); await outputFile.writeAsBytes(bytes); return outputFile.path; } }
错误日志
I/CCodec (26189): state->set(FLUSHING) I/CCodec (26189): state->set(FLUSHED) I/CCodec (26189): state->set(RESUMING) I/CCodecConfig(26189): query failed after returning 7 values (BAD_INDEX) W/Codec2Client(26189): query -- param skipped: index = 1342179345. W/Codec2Client(26189): query -- param skipped: index = 2415921170. I/CCodec (26189): state->set(RUNNING) I/CCodecBufferChannel(26189): [c2.sec.mp3.decoder#558] 4 initial input buffers available I/CCodec (26189): state->set(FLUSHING) I/CCodec (26189): state->set(FLUSHED) I/CCodec (26189): state->set(RESUMING) I/CCodecConfig(26189): query failed after returning 7 values (BAD_INDEX) W/Codec2Client(26189): query -- param skipped: index = 1342179345. W/Codec2Client(26189): query -- param skipped: index = 2415921170. I/CCodec (26189): state->set(RUNNING) I/CCodecBufferChannel(26189): [c2.sec.mp3.decoder#558] 4 initial input buffers available D/CCodecBuffers(26189): [c2.android.aac.decoder#447:1D-Input.Impl[N]] codec released a buffer owned by client (index 2) I/CCodec (26189): state->set(FLUSHING) I/CCodec (26189): state->set(FLUSHED) I/CCodec (26189): state->set(RESUMING) I/CCodecConfig(26189): query failed after returning 7 values (BAD_INDEX) W/Codec2Client(26189): query -- param skipped: index = 1342179345. W/Codec2Client(26189): query -- param skipped: index = 2415921170. I/CCodec (26189): state->set(RUNNING) I/CCodecBufferChannel(26189): [c2.sec.mp3.decoder#558] 4 initial input buffers available D/NativeAudioDecoder(26189): dequeueOutputBuffer timed out! /data/user/0/com.proshort.enterprise/app_flutter/summer_walk.mp3 I/CCodec (26189): state->set(FLUSHING) I/CCodec (26189): state->set(FLUSHED) I/CCodec (26189): state->set(RESUMING) I/CCodecConfig(26189): query failed after returning 7 values (BAD_INDEX) W/Codec2Client(26189): query -- param skipped: index = 1342179345. W/Codec2Client(26189): query -- param skipped: index = 2415921170. I/CCodec (26189): state->set(RUNNING) I/CCodecBufferChannel(26189): [c2.sec.mp3.decoder#558] 4 initial input buffers available D/NativeAudioDecoder(26189): dequeueOutputBuffer timed out! /data/user/0/com.proshort.enterprise/app_flutter/summer_walk.mp3 I/ViewRootImpl@2379ad7[FlutterVESDKActivity](26189): ViewPostIme pointer 0 I/ViewRootImpl@2379ad7[FlutterVESDKActivity](26189): ViewPostIme pointer 1 E/AndroidRuntime(26189): FATAL EXCEPTION: Thread-59 E/AndroidRuntime(26189): Process: com.proshort.enterprise, PID: 26189 E/AndroidRuntime(26189): java.io.FileNotFoundException E/AndroidRuntime(26189): at ly.img.android.pesdk.backend.decoder.Decoder.getUncachedInputStream(Decoder.java:151) E/AndroidRuntime(26189): at ly.img.android.pesdk.utils.UriHelper.copyAsFile(UriHelper.kt:63) E/AndroidRuntime(26189): at ly.img.android.pesdk.utils.UriHelper.copyAsFile$default(UriHelper.kt:62) E/AndroidRuntime(26189): at ly.img.android.pesdk.utils.UriHelper.convertToLocalUri(UriHelper.kt:207) E/AndroidRuntime(26189): at ly.img.android.pesdk.utils.DownloadUtils.downloadIfNeeded(DownloadUtils.kt:67) E/AndroidRuntime(26189): at ly.img.android.pesdk.backend.decoder.AudioSource.cacheUriIfNeeded(AudioSource.kt:88) E/AndroidRuntime(26189): at ly.img.android.pesdk.ui.panels.AudioGalleryToolPanel$onBeforeDetach$$inlined$runAsync$1.run(ThreadUtils.kt:435) E/AndroidRuntime(26189): at java.lang.Thread.run(Thread.java:1012) D/AudioTrack(26189): stop(1061): called with 74424 frames delivered D/BufferPoolAccessor2.0(26189): bufferpool2 0xe1ec6ed8 : 5(40960 size) total buffers - 4(32768 size) used buffers - 0/5 (recycle/alloc) - 4/800 (fetch/transfer) D/BufferPoolAccessor2.0(26189): evictor expired: 1, evicted: 1 E/FirebaseCrashlytics(26189): Cannot send reports. Timed out while fetching settings. I/Process (26189): Sending signal. PID: 26189 SIG: 9
解决方案
1. 传递file://格式的Uri给AudioClip
Android端SDK无法直接识别本地文件路径字符串,需要转换为file://开头的Uri格式,避免SDK将路径当作远程URL尝试下载:
final List<AudioClip> audioClips = audioSources .asMap() .entries .map((clip) => AudioClip( clip.value, Uri.file(localPaths[clip.key]).toString(), // 转换为file Uri title: clip.value.replaceAll('_', ' ').toCapitalize().toString())) .toList();
2. 禁用SDK自动下载逻辑
由于已提前下载音频文件,关闭SDK的自动下载功能,防止重复触发下载流程:
final audioOptions = AudioOptions( categories: [AudioClipCategory("", "", items: audioClips)], downloadEnabled: false); // 禁用自动下载
3. 确保文件下载完整
修改下载函数,添加状态码校验和更可靠的文件写入方式,避免损坏的音频文件导致解码超时:
// 替换原下载逻辑部分 final response = await request.close(); if (response.statusCode == HttpStatus.ok) { await response.pipe(outputFile.openWrite()); } else { throw Exception('下载失败,状态码:${response.statusCode}'); }
4. 显式设置文件权限
下载完成后设置文件可读权限,确保SDK能正常读取文件:
await outputFile.writeAsBytes(bytes); // 设置文件可读权限 await outputFile.chmod(0644);
内容的提问来源于stack exchange,提问作者Sankit Saini
相关产品推荐
相关产品推荐

