diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac5aa98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..f94fbcc --- /dev/null +++ b/.metadata @@ -0,0 +1,33 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "54e66469a933b60ddf175f858f82eaeb97e48c8d" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: android + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + - platform: ios + create_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + base_revision: 54e66469a933b60ddf175f858f82eaeb97e48c8d + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md index 001a15c..087ae06 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,239 @@ -# video_decode_plugin +# Video Decode Plugin + +> 当前版本:0.0.1 +> 最低 Flutter SDK 要求:>=3.3.0 +> 最低 Dart SDK 要求:>=3.3.4 +## 说明 +- 目前只支持Android端解码 -## Getting started +## 安装 -To make it easy for you to get started with GitLab, here's a list of recommended next steps. - -Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)! - -## Add your files - -- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files -- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command: - -``` -cd existing_repo -git remote add origin http://code.star-lock.cn/liyi/video_decode_plugin.git -git branch -M main -git push -uf origin main +```yaml +dependencies: + video_decode_plugin: ^0.0.1 # 使用最新版本 ``` -## Integrate with your tools +## 环境要求 -- [ ] [Set up project integrations](http://code.star-lock.cn/liyi/video_decode_plugin/-/settings/integrations) +- Flutter SDK: >=3.3.0 +- Dart SDK: >=3.3.4 +- Android: + - minSdkVersion: 21 (Android 5.0) + - targetSdkVersion: 最新版本 +- iOS: + - 最低版本: 11.0 + - 开发环境: Xcode 最新版本 -## Collaborate with your team +## 快速开始 -- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/) -- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html) -- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically) -- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/) -- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html) +### 在星锁项目中调试 +1. 需切换到`develop_liyi`分支 -## Test and Deploy +2. 将本仓库代码拉取到本地后,在项目中增加本地的插件依赖 +```dart + video_decode_plugin: + path: ../video_decode_plugin +``` -Use the built-in continuous integration in GitLab. +3. `appRouters.dart`找到打开路由配置文件,在文件最下方切换配置路由地址 +```dart + // GetPage(name: Routers.h264WebView, page: () => TalkViewNativeDecodePage()), // 插件播放页面 + GetPage(name: Routers.h264WebView, page: () => H264WebView()), // webview播放页面 +``` +4. 使用插件调试则打开上一个注释,同时注释掉weview的播放页面 -- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html) -- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/) -- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html) -- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/) -- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html) +### 初始化解码器 -*** +```dart +import 'package:video_decode_plugin/video_decode_plugin.dart'; -# Editing this README +// 创建解码器配置 +final config = VideoDecoderConfig( + width: 640, // 视频宽度 + height: 480, // 视频高度 + codecType: CodecType.h264, // 编解码类型:h264或h265 + frameRate: 30, // 目标帧率(可选) + isDebug: true, // 是否启用详细日志 +); -When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template. +// 初始化解码器,获取纹理ID +final textureId = await VideoDecodePlugin.initDecoder(config); -## Suggestions for a good README +// 设置帧回调 +VideoDecodePlugin.setFrameCallback((textureId) { + // 当新帧可用时被调用 + setState(() { + // 更新UI + }); +}); +``` -Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information. +### 渲染视频 -## Name -Choose a self-explaining name for your project. +```dart +// 使用Flutter的Texture组件显示视频 +Texture( + textureId: textureId, + filterQuality: FilterQuality.low, +) +``` -## Description -Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors. +### 解码视频帧 -## Badges -On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge. +```dart +// 解码I帧 +await VideoDecodePlugin.decodeFrame( + frameData, // Uint8List类型的H.264/H.265帧数据 + FrameType.iFrame +); -## Visuals -Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method. +// 解码P帧 +await VideoDecodePlugin.decodeFrame( + frameData, // Uint8List类型的H.264/H.265帧数据 + FrameType.pFrame +); +``` -## Installation -Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection. +### 获取解码统计信息 -## Usage -Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README. +```dart +final stats = await VideoDecodePlugin.getDecoderStats(textureId); +print('已渲染帧数: ${stats['renderedFrames']}'); +print('丢弃帧数: ${stats['droppedFrames']}'); +``` -## Support -Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc. +### 释放资源 -## Roadmap -If you have ideas for releases in the future, it is a good idea to list them in the README. +```dart +await VideoDecodePlugin.releaseDecoder(); +``` -## Contributing -State if you are open to contributions and what your requirements are for accepting them. +## 高级用法 -For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self. +### 多实例支持 -You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser. +插件支持同时创建和管理多个解码器实例: -## Authors and acknowledgment -Show your appreciation to those who have contributed to the project. +```dart +// 创建第一个解码器 +final textureId1 = await VideoDecodePlugin.createDecoder(config1); + +// 创建第二个解码器 +final textureId2 = await VideoDecodePlugin.createDecoder(config2); + +// 为特定纹理ID设置回调 +VideoDecodePlugin.setFrameCallbackForTexture(textureId1, (id) { + // 处理第一个解码器的帧 +}); + +// 为特定纹理ID解码帧 +await VideoDecodePlugin.decodeFrameForTexture(textureId2, frameData, frameType); + +// 释放特定解码器 +await VideoDecodePlugin.releaseDecoderForTexture(textureId1); +``` + +### 优化I帧和SPS/PPS处理 + +对于H.264视频流,建议按照以下顺序处理帧: + +1. 首先发送SPS(序列参数集,NAL类型7) +2. 其次发送PPS(图像参数集,NAL类型8) +3. 然后发送IDR帧(即I帧,NAL类型5) +4. 最后发送P帧(NAL类型1) + +```dart +// 发送SPS和PPS数据 +await VideoDecodePlugin.decodeFrame(spsData, FrameType.iFrame); +await VideoDecodePlugin.decodeFrame(ppsData, FrameType.iFrame); + +// 发送IDR帧 +await VideoDecodePlugin.decodeFrame(idrData, FrameType.iFrame); + +// 发送P帧 +await VideoDecodePlugin.decodeFrame(pFrameData, FrameType.pFrame); +``` + +## 完整示例 + +请参考示例应用,了解如何: +- 从文件或网络流加载H.264视频 +- 正确解析和处理NAL单元 +- 高效地解码和渲染视频帧 +- 监控解码性能并进行故障排除 + +## 版本历史 + +### 0.0.1 - 2025.04 +- ✨ 初始版本发布 +- 🎯 基础功能实现: + - H.264/H.265 解码支持 + - 实时视频流解码 + - 基础错误处理 + + +## 插件架构与职责 + +本插件为Flutter平台下的高性能、可扩展、易调试的视频解码渲染插件,专注于Android端H.264/H.265实时流解码。 + +- **Flutter端职责**: + - 负责NALU分割、SPS/PPS/I帧依赖链管理、滑动窗口丢帧、业务层帧流控制。 + - 依赖链完整性校验,P帧仅在依赖I帧已解码时才推送。 + - 通过MethodChannel将帧数据、类型、序号等元数据传递到原生端。 +- **Android端职责**: + - 极简高效解码与渲染,主线程安全回调。 + - 兜底依赖链校验,解码器生命周期管理,异常自愈。 + +## 文件结构与说明 + +### Dart端(lib/) +- **video_decode_plugin.dart**:插件主入口,负责解码器初始化、帧推送、回调注册、依赖链管理、与原生通信等。 +- **frame_dependency_manager.dart**:SPS/PPS/I帧依赖链滑动窗口管理,支持I帧序号窗口、依赖校验、SPS/PPS缓存。 +- **nalu_utils.dart**:NALU分割与类型识别工具,支持H.264/H.265帧的NALU单元分离、类型提取。 +- **video_decode_plugin_platform_interface.dart**:插件平台接口定义,支持多平台扩展,默认实现为MethodChannel。 +- **video_decode_plugin_method_channel.dart**:插件MethodChannel实现,负责与原生端通信。 + +### Android端(android/src/main/kotlin/top/skychip/video_decode_plugin/) +- **VideoDecodePlugin.kt**:插件原生入口,注册MethodChannel,管理解码器生命周期,处理Flutter端方法调用。 +- **VideoDecoder.kt**:解码器核心实现,负责MediaCodec解码、输入/输出队列、渲染线程、依赖链兜底校验、主线程回调、资源释放。 +- **VideoDecoderConfig.kt**:解码器配置参数定义,支持分辨率、编解码类型、帧率、调试模式等。 + +## H.264解码注意事项 + +### 1. 马赛克/花屏出现的根因 +- **依赖链断裂**:P/B帧依赖的I帧未被解码成功,或I帧本身丢失/损坏,后续P/B帧全部解码失败,必然花屏。 +- **SPS/PPS/I帧推送顺序错误**:未按SPS→PPS→I帧顺序推送,或I帧前未收到最新SPS/PPS,解码器无法正确解码I帧。 +- **解码链断裂后未及时reset解码器**:解码器内部状态异常,后续即使收到新I帧也无法恢复,需reset解码器。 + +### 2. 外部调用者(业务方)必须做好的前置操作 +- **推送顺序**:务必保证每个I帧前都已推送最新SPS(NAL类型7)、PPS(NAL类型8),再推送I帧(NAL类型5),最后推送P/B帧。 +- **依赖链完整性校验**:业务端需实现滑动窗口I帧序号管理,P帧推送前校验refIFrameSeq是否在窗口内,否则丢弃。 +- **丢帧策略**:强烈建议业务端实现丢帧与依赖链管理,避免无效帧流入原生端。 +- **异常自愈**:检测到解码失败/花屏/长时间无I帧时,建议主动reset解码器,等待下一个I帧恢复链路。 +- **日志监控**:建议业务端和原生端均输出详细日志,便于端到端排查依赖链断裂、丢包、乱序等问题。 + +### 3. 推荐推送流程(伪代码) +```dart +// 发送SPS和PPS +await VideoDecodePlugin.sendFrame(frameData: sps, frameType: 0, ...); +await VideoDecodePlugin.sendFrame(frameData: pps, frameType: 0, ...); +// 发送I帧 +await VideoDecodePlugin.sendFrame(frameData: iFrame, frameType: 0, ...); +// 发送P帧 +await VideoDecodePlugin.sendFrame(frameData: pFrame, frameType: 1, refIFrameSeq: iFrameSeq, ...); +``` + +## iOS端视频解码与渲染实现说明 + +- iOS端基于VideoToolbox实现H264/H265硬件解码,输出CVPixelBuffer。 +- 插件内部实现了解码输入缓冲区、输出缓冲区,解码与渲染完全解耦。 +- 独立渲染线程定时从输出缓冲区取帧,刷新Flutter纹理,支持EMA自适应帧率平滑调整,提升流畅度与健壮性。 +- P帧推送前会校验依赖的I帧是否已解码,若依赖链断裂则P帧直接丢弃,避免马赛克。 +- 仅推送标准NALU(type=1的P帧、type=5的I帧、type=7/8的SPS/PPS)进入解码器,SEI等异常NALU自动丢弃。 +- Flutter端建议用AspectRatio、FittedBox等包裹Texture,确保宽高比一致,避免白边。 +- 由于Flutter Texture机制无法直接控制原生UIView属性,建议Flutter端容器背景色设为透明,布局自适应。 +- 如需更强原生控制力,可考虑自定义PlatformView方案。 -## License -For open source projects, say how it is licensed. -## Project status -If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers. diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..3c0fff2 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,83 @@ +group 'top.skychip.video_decode_plugin' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '1.7.10' + repositories { + google() + mavenCentral() + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/jcenter' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + } + + dependencies { + classpath 'com.android.tools.build:gradle:7.3.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/jcenter' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'top.skychip.video_decode_plugin' + } + + compileSdk 34 + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main { + java { + // 排除Java源码目录 + exclude '**/java/**' + } + kotlin { + srcDirs += 'src/main/kotlin' + } + } + } + + defaultConfig { + minSdkVersion 19 + } + + dependencies { + testImplementation 'org.jetbrains.kotlin:kotlin-test' + testImplementation 'org.mockito:mockito-core:5.0.0' + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..50814e8 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,25 @@ +pluginManagement { + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/jcenter' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + google() + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + maven { url 'https://maven.aliyun.com/repository/google' } + maven { url 'https://maven.aliyun.com/repository/jcenter' } + maven { url 'https://maven.aliyun.com/repository/public' } + maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } + google() + mavenCentral() + } +} + +rootProject.name = 'video_decode_plugin' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..bc06b81 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + diff --git a/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecodePlugin.kt b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecodePlugin.kt new file mode 100644 index 0000000..bba254b --- /dev/null +++ b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecodePlugin.kt @@ -0,0 +1,121 @@ +package top.skychip.video_decode_plugin + +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.util.Log +import androidx.annotation.NonNull +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.view.TextureRegistry +import java.util.concurrent.ConcurrentHashMap +import java.util.HashSet + +/** 视频解码插件 */ +class VideoDecodePlugin : FlutterPlugin, MethodCallHandler { + private val TAG = "VideoDecodePlugin" + + // 方法通道 + private lateinit var channel: MethodChannel + + // Flutter上下文 + private lateinit var context: Context + + // 纹理注册表 + private lateinit var textureRegistry: TextureRegistry + + // 解码器 + private var decoder: VideoDecoder? = null + + // 纹理ID + private var textureId: Long? = null + + /** + * 插件绑定到Flutter引擎时调用 + */ + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + // 保存上下文和纹理注册表 + context = flutterPluginBinding.applicationContext + textureRegistry = flutterPluginBinding.textureRegistry + + // 创建方法通道 + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "video_decode_plugin") + channel.setMethodCallHandler(this) + } + + /** + * 处理Flutter方法调用 + */ + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + when (call.method) { + "initDecoder" -> handleInitDecoder(call, result) + "decodeFrame" -> handleDecodeFrame(call, result) + "releaseDecoder" -> handleReleaseDecoder(call, result) + else -> result.notImplemented() + } + } + + /** + * 初始化解码器 + */ + private fun handleInitDecoder(call: MethodCall, result: Result) { + try { + val width = call.argument("width") ?: 640 + val height = call.argument("height") ?: 360 + val codecType = call.argument("codecType") ?: "h264" + val textureEntry = textureRegistry.createSurfaceTexture() + textureId = textureEntry.id() + decoder = VideoDecoder(context, textureEntry, width, height, codecType) { + // onFrameRendered callback + channel.invokeMethod("onFrameRendered", mapOf("textureId" to textureId)) + } + result.success(textureId) + } catch (e: Exception) { + result.error("INIT_FAILED", "初始化解码器失败: ${e.message}", null) + } + } + + /** + * 解码视频帧 + */ + private fun handleDecodeFrame(call: MethodCall, result: Result) { + try { + val frameData = call.argument("frameData") ?: return result.error("INVALID_ARGS", "无效的帧数据", null) + val frameType = call.argument("frameType") ?: 0 + val timestamp = (call.argument("timestamp"))?.toLong() ?: 0L + val frameSeq = call.argument("frameSeq") ?: 0 + val refIFrameSeq = call.argument("refIFrameSeq") + val success = decoder?.decodeFrame(frameData, frameType, timestamp, frameSeq, refIFrameSeq) ?: false + result.success(success) + } catch (e: Exception) { + result.error("DECODE_FAILED", "解码帧失败: ${e.message}", null) + } + } + + /** + * 释放解码器资源 + */ + private fun handleReleaseDecoder(call: MethodCall, result: Result) { + try { + decoder?.release() + decoder = null + textureId = null + result.success(true) + } catch (e: Exception) { + result.error("RELEASE_FAILED", "释放解码器失败: ${e.message}", null) + } + } + + /** + * 插件从Flutter引擎解绑时调用 + */ + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + decoder?.release() + decoder = null + textureId = null + channel.setMethodCallHandler(null) + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt new file mode 100644 index 0000000..3c0bdbf --- /dev/null +++ b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt @@ -0,0 +1,359 @@ +package top.skychip.video_decode_plugin + +import android.content.Context +import android.graphics.SurfaceTexture +import android.media.MediaCodec +import android.media.MediaFormat +import android.os.Handler +import android.os.Looper +import android.util.Log +import android.view.Surface +import io.flutter.view.TextureRegistry +import java.nio.ByteBuffer +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import java.util.Collections +import java.util.concurrent.ConcurrentHashMap +import android.os.SystemClock +import java.util.ArrayDeque + +/** + * 视频解码器核心类。 + * 主要职责: + * 1. 初始化并配置Android MediaCodec解码器,支持H264/H265视频流解码。 + * 2. 管理解码输入帧队列,将解码后的视频帧渲染到Surface。 + * 3. 支持多线程解码与渲染,解耦数据流。 + * 4. 负责解码器的生命周期管理(启动、释放等)。 + * 5. 通过回调通知Flutter端有新帧渲染。 + * + * 构造参数说明: + * - context: Android上下文 + * - textureEntry: Flutter纹理注册表的SurfaceTextureEntry,用于视频渲染 + * - width: 视频宽度 + * - height: 视频高度 + * - codecType: 编解码器类型("h264"或"h265") + * - onFrameRendered: 开始解码并且渲染成功后的回调 + */ +class VideoDecoder( + context: Context, + textureEntry: TextureRegistry.SurfaceTextureEntry, + width: Int, + height: Int, + codecType: String, + private val onFrameRendered: () -> Unit +) { + companion object { + private const val TAG = "VideoDecoder" + private const val TIMEOUT_US = 10000L + private const val INPUT_BUFFER_QUEUE_CAPACITY = 30 // 输入缓冲区容量 + } + + // region 成员变量定义 + + // SurfaceTexture与Surface用于视频渲染 + private val surfaceTexture: SurfaceTexture = textureEntry.surfaceTexture() + private val surface: Surface = Surface(surfaceTexture) + private var mediaCodec: MediaCodec? = null + + // 输入帧队列,支持并发,容量较大以防止丢帧 + private val inputFrameQueue = LinkedBlockingQueue(INPUT_BUFFER_QUEUE_CAPACITY) + private var running = true // 解码器运行状态 + private val frameSeqSet = Collections.newSetFromMap(ConcurrentHashMap()) // 防止重复帧入队 + + // 解码输出缓冲区,容量为100帧 + private val outputFrameQueue = LinkedBlockingQueue(17) + + // 渲染线程控制 + @Volatile private var renderThreadRunning = true + private var renderThread: Thread? = null + + // 主线程Handler,用于安全切换onFrameRendered到主线程 + private val mainHandler = Handler(Looper.getMainLooper()) + + // 渲染帧率(fps),可由外部控制,默认18 + @Volatile var renderFps: Int = 15 + + // 兜底:记录最近一次I帧的frameSeq,P/B帧依赖校验 + @Volatile private var lastIFrameSeq: Int? = null + + // 解码输出帧时间戳队列(用于动态帧率统计和平滑) + private val decodeTimestampQueue = ArrayDeque(20) // 最多保存20帧时间戳 + private val decodeTimestampLock = ReentrantLock() // 线程安全保护 + + // EMA平滑参数 + @Volatile private var smoothedFps: Double = 15.0 // 平滑后的渲染帧率 + private val alpha = 0.2 // EMA平滑系数,越大响应越快 + private val minFps = 8 // 渲染帧率下限,防止过低 + private val maxFps = 30 // 渲染帧率上限,防止过高 + private val maxStep = 2.0 // 单次最大调整幅度,防止突变 + + // 1. 新增成员变量 + @Volatile private var latestRenderedTimestampMs: Long? = null + private val MAX_ALLOWED_DELAY_MS = 350 // 最大允许延迟,单位毫秒 + @Volatile private var timestampBaseMs: Long? = null + @Volatile private var firstFrameRelativeTimestamp: Long? = null + + // 输入帧结构体 + private data class FrameData( + val data: ByteArray, + val frameType: Int, + val timestamp: Long, + val frameSeq: Int, + val refIFrameSeq: Int? + ) + + // 解码后帧结构体,显式携带时间戳(单位:微秒) + private data class DecodedFrame( + val codec: MediaCodec, + val bufferIndex: Int, + val info: MediaCodec.BufferInfo, + val timestampUs: Long // 帧时间戳,单位微秒 + ) + + // endregion + + // region 初始化与解码器配置 + init { + // 配置Surface尺寸 + surfaceTexture.setDefaultBufferSize(width, height) + // 选择MIME类型 + val mime = when (codecType) { + "h264" -> "video/avc" + "h265" -> "video/hevc" + else -> "video/avc" + } + // 创建并配置MediaFormat + val format = MediaFormat.createVideoFormat(mime, width, height) + format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, width * height) + // 创建解码器 + val decoder = MediaCodec.createDecoderByType(mime) + // 设置解码回调 + decoder.setCallback(object : MediaCodec.Callback() { + override fun onInputBufferAvailable(codec: MediaCodec, index: Int) { + if (!running) return + // 从输入队列取出一帧数据 + val frame = inputFrameQueue.poll() + if (frame != null) { + // 5. 取绝对时间戳 + val base = timestampBaseMs ?: 0L + val firstRel = firstFrameRelativeTimestamp ?: 0L + val absTimestamp = base + (frame.timestamp - firstRel) + val now = System.currentTimeMillis() + if (absTimestamp < now - MAX_ALLOWED_DELAY_MS) { + Log.w(TAG, "[onInputBufferAvailable] Drop frame due to delay: absFrameTs=$absTimestamp, now=$now, maxDelay=$MAX_ALLOWED_DELAY_MS") + frameSeqSet.remove(frame.frameSeq) + codec.queueInputBuffer(index, 0, 0, 0, 0) + return + } + frameSeqSet.remove(frame.frameSeq) + val inputBuffer = codec.getInputBuffer(index) + if (inputBuffer != null) { + inputBuffer.clear() + inputBuffer.put(frame.data) + val start = System.nanoTime() + val ptsUs = absTimestamp * 1000L // 6. 送入解码器用绝对时间戳 + codec.queueInputBuffer( + index, + 0, + frame.data.size, + ptsUs, + if (frame.frameType == 0) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0 + ) + } else { + codec.queueInputBuffer(index, 0, 0, 0, 0) + } + } else { + codec.queueInputBuffer(index, 0, 0, 0, 0) + } + } + override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { + if (!running) return + // 记录解码输出时间戳 + val now = SystemClock.elapsedRealtime() + decodeTimestampLock.withLock { + if (decodeTimestampQueue.size >= 20) { + decodeTimestampQueue.removeFirst() + } + decodeTimestampQueue.addLast(now) + } + // 解码后帧入输出缓冲区,由渲染线程处理 + val frame = DecodedFrame(codec, index, MediaCodec.BufferInfo().apply { + set(0, info.size, info.presentationTimeUs, info.flags) + }, info.presentationTimeUs) + if (!outputFrameQueue.offer(frame)) { + // 缓冲区满,丢弃最旧帧再插入 + outputFrameQueue.poll() + outputFrameQueue.offer(frame) + } + } + override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { + Log.e(TAG, "MediaCodec error", e) + } + override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {} + }) + decoder.configure(format, surface, null, 0) + decoder.start() + mediaCodec = decoder + + // 启动渲染线程 + renderThreadRunning = true + renderThread = Thread { + var hasNotifiedFlutter = false + var renderedFrameCount = 0 // 渲染帧计数器 + val fpsAdjustInterval = 10 // 每渲染10帧调整一次帧率 + while (renderThreadRunning) { + // 计算每帧渲染间隔 + val frameIntervalMs = if (renderFps > 0) 1000L / renderFps else 66L + val loopStart = SystemClock.elapsedRealtime() + try { + // 阻塞式等待新帧,避免空转 + val frame = outputFrameQueue.take() + frame.codec.releaseOutputBuffer(frame.bufferIndex, true) + // 7. 渲染线程用系统时间推进 + latestRenderedTimestampMs = System.currentTimeMillis() + renderedFrameCount++ + // 只在首次渲染时回调Flutter + if (!hasNotifiedFlutter) { + mainHandler.post { onFrameRendered() } + hasNotifiedFlutter = true + } + // 每渲染N帧动态调整一次帧率 + if (renderedFrameCount % fpsAdjustInterval == 0) { + val measuredFps = calculateDecodeFps() + val newFps = updateSmoothedFps(measuredFps) + renderFps = newFps + Log.i(TAG, "[AutoFps] measuredFps=$measuredFps, smoothedFps=$smoothedFps, renderFps=$renderFps") + } + } catch (e: Exception) { + Log.e(TAG, "[RenderThread] Exception", e) + } + // 控制渲染节奏 + val loopCost = SystemClock.elapsedRealtime() - loopStart + val sleepMs = frameIntervalMs - loopCost + if (sleepMs > 0) { + try { Thread.sleep(sleepMs) } catch (_: Exception) {} + } else { + // 若解码极慢,sleepMs为负,直接进入下一帧,防止阻塞 + } + } + // 清理剩余帧,防止内存泄漏 + while (true) { + val frame = outputFrameQueue.poll() ?: break + try { + frame.codec.releaseOutputBuffer(frame.bufferIndex, false) + } catch (_: Exception) {} + } + } + renderThread?.start() + } + // endregion + + // region 核心方法 + + /** + * 向解码器输入一帧数据(所有类型均允许入队,去重) + */ + fun decodeFrame( + frameData: ByteArray, + frameType: Int, + timestamp: Long, // 单位:毫秒,要求外部递增 + frameSeq: Int, + refIFrameSeq: Int? + ): Boolean { + if (!running || mediaCodec == null) return false + if (!frameSeqSet.add(frameSeq)) return false // 防止重复帧 + // 2. 初始化起点 + if (timestampBaseMs == null) { + synchronized(this) { + if (timestampBaseMs == null) { + timestampBaseMs = System.currentTimeMillis() + firstFrameRelativeTimestamp = timestamp + Log.i(TAG, "[timestampBase] Set timestampBaseMs=$timestampBaseMs, firstFrameRelativeTimestamp=$firstFrameRelativeTimestamp") + } + } + } + val base = timestampBaseMs ?: 0L + val firstRel = firstFrameRelativeTimestamp ?: 0L + val absTimestamp = base + (timestamp - firstRel) + // 3. decodeFrame延迟丢弃判断(用系统时间) + val now = System.currentTimeMillis() + val diff = now - absTimestamp + // Log.d(TAG, "[decodeFrame] absTimestamp=$absTimestamp, now=$now, now-absTimestamp=$diff, maxDelay=$MAX_ALLOWED_DELAY_MS") + if (absTimestamp < now - MAX_ALLOWED_DELAY_MS) { + Log.w(TAG, "[decodeFrame] Drop frame due to delay: absFrameTs=$absTimestamp, now=$now, maxDelay=$MAX_ALLOWED_DELAY_MS") + return false + } + var allow = false + if (frameType == 0) { // I帧 + lastIFrameSeq = frameSeq + allow = true + } else { + val lastI = lastIFrameSeq + if (lastI == null) { + Log.w(TAG, "[decodeFrame] Drop P/B frame: no I-frame yet, frameSeq=$frameSeq, refIFrameSeq=$refIFrameSeq") + return false + } + allow = (refIFrameSeq == lastI) + if (!allow) { + Log.w(TAG, "[decodeFrame] Drop P/B frame: refIFrameSeq=$refIFrameSeq != lastIFrameSeq=$lastI, frameSeq=$frameSeq") + return false + } + } + // 4. 入队时FrameData仍用原始相对时间戳 + return inputFrameQueue.offer(FrameData(frameData, frameType, timestamp, frameSeq, refIFrameSeq), 50, TimeUnit.MILLISECONDS) + } + + /** + * 释放解码器和相关资源 + */ + fun release() { + running = false + inputFrameQueue.clear() + // 停止渲染线程 + renderThreadRunning = false + try { + renderThread?.join(200) + } catch (_: Exception) {} + outputFrameQueue.clear() + try { + mediaCodec?.stop() + mediaCodec?.release() + } catch (_: Exception) {} + try { + surface.release() + } catch (_: Exception) {} + } + + /** + * 计算最近N帧的平均解码帧率(fps) + */ + private fun calculateDecodeFps(): Double { + decodeTimestampLock.withLock { + if (decodeTimestampQueue.size < 2) return renderFps.toDouble() + val first = decodeTimestampQueue.first() + val last = decodeTimestampQueue.last() + val frameCount = decodeTimestampQueue.size - 1 + val durationMs = (last - first).coerceAtLeast(1L) + return frameCount * 1000.0 / durationMs + } + } + + /** + * EMA平滑更新渲染帧率 + * @param measuredFps 当前测得的解码帧率 + * @return 平滑后的渲染帧率(取整) + */ + private fun updateSmoothedFps(measuredFps: Double): Int { + // measuredFps边界保护 + val safeFps = measuredFps.coerceIn(minFps.toDouble(), maxFps.toDouble()) + val targetFps = alpha * safeFps + (1 - alpha) * smoothedFps + val delta = targetFps - smoothedFps + val step = delta.coerceIn(-maxStep, maxStep) + smoothedFps = (smoothedFps + step).coerceIn(minFps.toDouble(), maxFps.toDouble()) + return smoothedFps.toInt() + } + // endregion +} \ No newline at end of file diff --git a/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoderConfig.kt b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoderConfig.kt new file mode 100644 index 0000000..2d55cbb --- /dev/null +++ b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoderConfig.kt @@ -0,0 +1,20 @@ +package top.skychip.video_decode_plugin + +/** + * 视频解码器配置 + * + * @param width 视频宽度 + * @param height 视频高度 + * @param codecType 编解码器类型,默认为h264 + * @param frameRate 帧率,可为空 + * @param isDebug 是否开启调试日志 + * @param isAsync 是否使用异步解码模式,默认为true + */ +data class VideoDecoderConfig( + val width: Int, + val height: Int, + val codecType: String = "h264", + val frameRate: Int? = null, + val isDebug: Boolean = false, + val isAsync: Boolean = true +) \ No newline at end of file diff --git a/android/src/test/kotlin/top/skychip/video_decode_plugin/VideoDecodePluginTest.kt b/android/src/test/kotlin/top/skychip/video_decode_plugin/VideoDecodePluginTest.kt new file mode 100644 index 0000000..0352d62 --- /dev/null +++ b/android/src/test/kotlin/top/skychip/video_decode_plugin/VideoDecodePluginTest.kt @@ -0,0 +1,27 @@ +package top.skychip.video_decode_plugin + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import kotlin.test.Test +import org.mockito.Mockito + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class VideoDecodePluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = VideoDecodePlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..29a3a50 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..6f57c2c --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# video_decode_plugin_example + +Demonstrates how to use the video_decode_plugin plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..6f56801 --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,13 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java + +# Remember to never publicly share your keystore. +# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle new file mode 100644 index 0000000..e372129 --- /dev/null +++ b/example/android/app/build.gradle @@ -0,0 +1,67 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + +def localProperties = new Properties() +def localPropertiesFile = rootProject.file('local.properties') +if (localPropertiesFile.exists()) { + localPropertiesFile.withReader('UTF-8') { reader -> + localProperties.load(reader) + } +} + +def flutterVersionCode = localProperties.getProperty('flutter.versionCode') +if (flutterVersionCode == null) { + flutterVersionCode = '1' +} + +def flutterVersionName = localProperties.getProperty('flutter.versionName') +if (flutterVersionName == null) { + flutterVersionName = '1.0' +} + +android { + namespace "top.skychip.video_decode_plugin_example" + compileSdk flutter.compileSdkVersion + ndkVersion flutter.ndkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId "top.skychip.video_decode_plugin_example" + // You can update the following values to match your application needs. + // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig signingConfigs.debug + } + } +} + +flutter { + source '../..' +} + +dependencies {} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ceb5e3f --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/top/skychip/video_decode_plugin_example/MainActivity.kt b/example/android/app/src/main/kotlin/top/skychip/video_decode_plugin_example/MainActivity.kt new file mode 100644 index 0000000..d0378b6 --- /dev/null +++ b/example/android/app/src/main/kotlin/top/skychip/video_decode_plugin_example/MainActivity.kt @@ -0,0 +1,5 @@ +package top.skychip.video_decode_plugin_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle b/example/android/build.gradle new file mode 100644 index 0000000..bc157bd --- /dev/null +++ b/example/android/build.gradle @@ -0,0 +1,18 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +rootProject.buildDir = '../build' +subprojects { + project.buildDir = "${rootProject.buildDir}/${project.name}" +} +subprojects { + project.evaluationDependsOn(':app') +} + +tasks.register("clean", Delete) { + delete rootProject.buildDir +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..df8a018 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,4 @@ +org.gradle.jvmargs=-Xmx4G +android.useAndroidX=true +android.enableJetifier=true +org.gradle.java.home=/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..4dcb03d --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://mirrors.cloud.tencent.com/gradle/gradle-8.5-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle new file mode 100644 index 0000000..1d6d19b --- /dev/null +++ b/example/android/settings.gradle @@ -0,0 +1,26 @@ +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + } + settings.ext.flutterSdkPath = flutterSdkPath() + + includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.3.0" apply false + id "org.jetbrains.kotlin.android" version "1.7.10" apply false +} + +include ":app" diff --git a/example/assets/demo.h264 b/example/assets/demo.h264 new file mode 100644 index 0000000..eb04d7b Binary files /dev/null and b/example/assets/demo.h264 differ diff --git a/example/ios/.gitignore b/example/ios/.gitignore new file mode 100644 index 0000000..7a7f987 --- /dev/null +++ b/example/ios/.gitignore @@ -0,0 +1,34 @@ +**/dgph +*.mode1v3 +*.mode2v3 +*.moved-aside +*.pbxuser +*.perspectivev3 +**/*sync/ +.sconsign.dblite +.tags* +**/.vagrant/ +**/DerivedData/ +Icon? +**/Pods/ +**/.symlinks/ +profile +xcuserdata +**/.generated/ +Flutter/App.framework +Flutter/Flutter.framework +Flutter/Flutter.podspec +Flutter/Generated.xcconfig +Flutter/ephemeral/ +Flutter/app.flx +Flutter/app.zip +Flutter/flutter_assets/ +Flutter/flutter_export_environment.sh +ServiceDefinitions.json +Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!default.mode1v3 +!default.mode2v3 +!default.pbxuser +!default.perspectivev3 diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist new file mode 100644 index 0000000..7c56964 --- /dev/null +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -0,0 +1,26 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + App + CFBundleIdentifier + io.flutter.flutter.app + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + App + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + 1.0 + MinimumOSVersion + 12.0 + + diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig new file mode 100644 index 0000000..ec97fc6 --- /dev/null +++ b/example/ios/Flutter/Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig new file mode 100644 index 0000000..c4855bf --- /dev/null +++ b/example/ios/Flutter/Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..d97f17e --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock new file mode 100644 index 0000000..0396125 --- /dev/null +++ b/example/ios/Podfile.lock @@ -0,0 +1,41 @@ +PODS: + - Flutter (1.0.0) + - integration_test (0.0.1): + - Flutter + - path_provider_foundation (0.0.1): + - Flutter + - FlutterMacOS + - permission_handler_apple (9.1.1): + - Flutter + - video_decode_plugin (0.0.1): + - Flutter + +DEPENDENCIES: + - Flutter (from `Flutter`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) + - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) + - video_decode_plugin (from `.symlinks/plugins/video_decode_plugin/ios`) + +EXTERNAL SOURCES: + Flutter: + :path: Flutter + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + path_provider_foundation: + :path: ".symlinks/plugins/path_provider_foundation/darwin" + permission_handler_apple: + :path: ".symlinks/plugins/permission_handler_apple/ios" + video_decode_plugin: + :path: ".symlinks/plugins/video_decode_plugin/ios" + +SPEC CHECKSUMS: + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 + integration_test: 2d03ab552da9a1f408709a6acf3d7ca4cb3cb307 + path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 + permission_handler_apple: 3787117e48f80715ff04a3830ca039283d6a4f29 + video_decode_plugin: 07649b4703fdf618daf7000af58f3b251c3e280f + +PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 + +COCOAPODS: 1.16.2 diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..57c5f1a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,742 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXBuildFile section */ + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 3CE7980F7865F7F8E5B7162F /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 521BE986C2B423CB297B1C00 /* Pods_RunnerTests.framework */; }; + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 8D7AB43641B575B850A72098 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F27CD0209D3FA843C2B3E9E /* Pods_Runner.framework */; }; + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 9705A1C41CF9048500538489 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1F27CD0209D3FA843C2B3E9E /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 21DF05E93201EF3DED427347 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4A2F7419BFF633E97CA4B2ED /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 521BE986C2B423CB297B1C00 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 6A7EFB3AABB3AB94044A2F39 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 6D4BFC721107664848B40160 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; + 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; + 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; + 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; + 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A46450F4F351DEEECD7F5A62 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + A84C216D2CDBF62BB951B792 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2AE63D1C6EB40BB145F7AFAE /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 3CE7980F7865F7F8E5B7162F /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EB1CF9000F007C117D /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 8D7AB43641B575B850A72098 /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 9740EEB11CF90186004384FC /* Flutter */ = { + isa = PBXGroup; + children = ( + 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 9740EEB31CF90195004384FC /* Generated.xcconfig */, + ); + name = Flutter; + sourceTree = ""; + }; + 97C146E51CF9000F007C117D = { + isa = PBXGroup; + children = ( + 9740EEB11CF90186004384FC /* Flutter */, + 97C146F01CF9000F007C117D /* Runner */, + 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + A8D9A7D8E0142B6FF06E6590 /* Pods */, + ACF8A28F7E0BB4CDED852419 /* Frameworks */, + ); + sourceTree = ""; + }; + 97C146EF1CF9000F007C117D /* Products */ = { + isa = PBXGroup; + children = ( + 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 97C146F01CF9000F007C117D /* Runner */ = { + isa = PBXGroup; + children = ( + 97C146FA1CF9000F007C117D /* Main.storyboard */, + 97C146FD1CF9000F007C117D /* Assets.xcassets */, + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, + 97C147021CF9000F007C117D /* Info.plist */, + 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, + 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, + 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + ); + path = Runner; + sourceTree = ""; + }; + A8D9A7D8E0142B6FF06E6590 /* Pods */ = { + isa = PBXGroup; + children = ( + 21DF05E93201EF3DED427347 /* Pods-Runner.debug.xcconfig */, + 6A7EFB3AABB3AB94044A2F39 /* Pods-Runner.release.xcconfig */, + A84C216D2CDBF62BB951B792 /* Pods-Runner.profile.xcconfig */, + 4A2F7419BFF633E97CA4B2ED /* Pods-RunnerTests.debug.xcconfig */, + A46450F4F351DEEECD7F5A62 /* Pods-RunnerTests.release.xcconfig */, + 6D4BFC721107664848B40160 /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + ACF8A28F7E0BB4CDED852419 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 1F27CD0209D3FA843C2B3E9E /* Pods_Runner.framework */, + 521BE986C2B423CB297B1C00 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 4E3E18A6B5EBAEE36E80FB44 /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 2AE63D1C6EB40BB145F7AFAE /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 97C146ED1CF9000F007C117D /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + ACFD06CDD74A2D9EC586D2CF /* [CP] Check Pods Manifest.lock */, + 9740EEB61CF901F6004384FC /* Run Script */, + 97C146EA1CF9000F007C117D /* Sources */, + 97C146EB1CF9000F007C117D /* Frameworks */, + 97C146EC1CF9000F007C117D /* Resources */, + 9705A1C41CF9048500538489 /* Embed Frameworks */, + 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + C1DA649D0A31D2DCC95D2BAA /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Runner; + productName = Runner; + productReference = 97C146EE1CF9000F007C117D /* Runner.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 97C146E61CF9000F007C117D /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; + 97C146ED1CF9000F007C117D = { + CreatedOnToolsVersion = 7.3.1; + LastSwiftMigration = 1100; + }; + }; + }; + buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 97C146E51CF9000F007C117D; + productRefGroup = 97C146EF1CF9000F007C117D /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EC1CF9000F007C117D /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, + 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, + 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + ); + name = "Thin Binary"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; + }; + 4E3E18A6B5EBAEE36E80FB44 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 9740EEB61CF901F6004384FC /* Run Script */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputPaths = ( + ); + name = "Run Script"; + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; + }; + ACFD06CDD74A2D9EC586D2CF /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + C1DA649D0A31D2DCC95D2BAA /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 97C146EA1CF9000F007C117D /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 97C146FA1CF9000F007C117D /* Main.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C146FB1CF9000F007C117D /* Base */, + ); + name = Main.storyboard; + sourceTree = ""; + }; + 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 97C147001CF9000F007C117D /* Base */, + ); + name = LaunchScreen.storyboard; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 249021D3217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Profile; + }; + 249021D4217E4FDB00AE95B9 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NAQ5PL2DYC; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.starlock.lock.local; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Debug_com.starlock.lock.local.mobileprovision; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Profile; + }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 4A2F7419BFF633E97CA4B2ED /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = A46450F4F351DEEECD7F5A62 /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 6D4BFC721107664848B40160 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; + 97C147031CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 97C147041CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + TARGETED_DEVICE_FAMILY = "1,2"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 97C147061CF9000F007C117D /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NAQ5PL2DYC; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.starlock.lock.local; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Debug_com.starlock.lock.local.mobileprovision; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Debug; + }; + 97C147071CF9000F007C117D /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = ""; + "DEVELOPMENT_TEAM[sdk=iphoneos*]" = NAQ5PL2DYC; + ENABLE_BITCODE = NO; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = com.starlock.lock.local; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + "PROVISIONING_PROFILE_SPECIFIER[sdk=iphoneos*]" = Debug_com.starlock.lock.local.mobileprovision; + SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + VERSIONING_SYSTEM = "apple-generic"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147031CF9000F007C117D /* Debug */, + 97C147041CF9000F007C117D /* Release */, + 249021D3217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 97C147061CF9000F007C117D /* Debug */, + 97C147071CF9000F007C117D /* Release */, + 249021D4217E4FDB00AE95B9 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 97C146E61CF9000F007C117D /* Project object */; +} diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..8e3ca5d --- /dev/null +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,98 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..21a3cc1 --- /dev/null +++ b/example/ios/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift new file mode 100644 index 0000000..70693e4 --- /dev/null +++ b/example/ios/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import UIKit +import Flutter + +@UIApplicationMain +@objc class AppDelegate: FlutterAppDelegate { + override func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + GeneratedPluginRegistrant.register(with: self) + return super.application(application, didFinishLaunchingWithOptions: launchOptions) + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..d36b1fa --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,122 @@ +{ + "images" : [ + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "20x20", + "idiom" : "iphone", + "filename" : "Icon-App-20x20@3x.png", + "scale" : "3x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "iphone", + "filename" : "Icon-App-29x29@3x.png", + "scale" : "3x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "iphone", + "filename" : "Icon-App-40x40@3x.png", + "scale" : "3x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@2x.png", + "scale" : "2x" + }, + { + "size" : "60x60", + "idiom" : "iphone", + "filename" : "Icon-App-60x60@3x.png", + "scale" : "3x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@1x.png", + "scale" : "1x" + }, + { + "size" : "20x20", + "idiom" : "ipad", + "filename" : "Icon-App-20x20@2x.png", + "scale" : "2x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@1x.png", + "scale" : "1x" + }, + { + "size" : "29x29", + "idiom" : "ipad", + "filename" : "Icon-App-29x29@2x.png", + "scale" : "2x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@1x.png", + "scale" : "1x" + }, + { + "size" : "40x40", + "idiom" : "ipad", + "filename" : "Icon-App-40x40@2x.png", + "scale" : "2x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@1x.png", + "scale" : "1x" + }, + { + "size" : "76x76", + "idiom" : "ipad", + "filename" : "Icon-App-76x76@2x.png", + "scale" : "2x" + }, + { + "size" : "83.5x83.5", + "idiom" : "ipad", + "filename" : "Icon-App-83.5x83.5@2x.png", + "scale" : "2x" + }, + { + "size" : "1024x1024", + "idiom" : "ios-marketing", + "filename" : "Icon-App-1024x1024@1x.png", + "scale" : "1x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png new file mode 100644 index 0000000..dc9ada4 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png new file mode 100644 index 0000000..7353c41 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png new file mode 100644 index 0000000..6ed2d93 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png new file mode 100644 index 0000000..4cd7b00 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png new file mode 100644 index 0000000..fe73094 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png new file mode 100644 index 0000000..321773c Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png new file mode 100644 index 0000000..797d452 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png new file mode 100644 index 0000000..502f463 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png new file mode 100644 index 0000000..0ec3034 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png new file mode 100644 index 0000000..e9f5fea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png new file mode 100644 index 0000000..84ac32a Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png new file mode 100644 index 0000000..8953cba Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png new file mode 100644 index 0000000..0467bf1 Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json new file mode 100644 index 0000000..0bedcf2 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "idiom" : "universal", + "filename" : "LaunchImage.png", + "scale" : "1x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@2x.png", + "scale" : "2x" + }, + { + "idiom" : "universal", + "filename" : "LaunchImage@3x.png", + "scale" : "3x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png new file mode 100644 index 0000000..9da19ea Binary files /dev/null and b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md new file mode 100644 index 0000000..89c2725 --- /dev/null +++ b/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md @@ -0,0 +1,5 @@ +# Launch Screen Assets + +You can customize the launch screen with your own desired assets by replacing the image files in this directory. + +You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard new file mode 100644 index 0000000..f2e259c --- /dev/null +++ b/example/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/example/ios/Runner/Base.lproj/Main.storyboard new file mode 100644 index 0000000..f3c2851 --- /dev/null +++ b/example/ios/Runner/Base.lproj/Main.storyboard @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist new file mode 100644 index 0000000..86f1cb3 --- /dev/null +++ b/example/ios/Runner/Info.plist @@ -0,0 +1,49 @@ + + + + + CADisableMinimumFrameDurationOnPhone + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Video Decode Plugin + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + video_decode_plugin_example + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSRequiresIPhoneOS + + UIApplicationSupportsIndirectInputEvents + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h new file mode 100644 index 0000000..308a2a5 --- /dev/null +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -0,0 +1 @@ +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..3e7dc6f --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,26 @@ +import Flutter +import UIKit +import XCTest + +@testable import video_decode_plugin + +// This demonstrates a simple unit test of the Swift portion of this plugin's implementation. +// +// See https://developer.apple.com/documentation/xctest for more information about using XCTest. + +class RunnerTests: XCTestCase { + + func testGetPlatformVersion() { + let plugin = VideoDecodePlugin() + + let call = FlutterMethodCall(methodName: "getPlatformVersion", arguments: []) + + let resultExpectation = expectation(description: "result block must be called.") + plugin.handle(call) { result in + XCTAssertEqual(result as! String, "iOS " + UIDevice.current.systemVersion) + resultExpectation.fulfill() + } + waitForExpectations(timeout: 1) + } + +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..35c9c6c --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,1078 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:math' as math; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:video_decode_plugin/video_decode_plugin.dart'; + +// 用于存储H264文件中解析出的帧 +class H264Frame { + final Uint8List data; + final int frameType; // 0=I帧, 1=P帧 + final int? refIFrameSeq; + H264Frame(this.data, this.frameType, [this.refIFrameSeq]); +} + +// H264 NAL 单元类型 +class NalUnitType { + static const int UNSPECIFIED = 0; + static const int CODED_SLICE_NON_IDR = 1; // P帧 + static const int PARTITION_A = 2; + static const int PARTITION_B = 3; + static const int PARTITION_C = 4; + static const int CODED_SLICE_IDR = 5; // I帧 + static const int SEI = 6; // 补充增强信息 + static const int SPS = 7; // 序列参数集 + static const int PPS = 8; // 图像参数集 + static const int AUD = 9; // 访问单元分隔符 + static const int END_SEQUENCE = 10; // 序列结束 + static const int END_STREAM = 11; // 码流结束 + static const int FILLER_DATA = 12; // 填充数据 + static const int SPS_EXT = 13; // SPS扩展 + static const int PREFIX_NAL = 14; // 前缀NAL单元 + static const int SUBSET_SPS = 15; // 子集SPS + static const int DEPTH_PARAM_SET = 16; // 深度参数集 + static const int RESERVED_17 = 17; // 保留类型 + static const int RESERVED_18 = 18; // 保留类型 + static const int CODED_SLICE_AUX = 19; // 辅助切片 + static const int CODED_SLICE_EXTENSION = 20; // 扩展切片 + static const int CODED_SLICE_DEPTH = 21; // 深度扩展 + static const int RESERVED_22 = 22; // 保留类型 + static const int RESERVED_23 = 23; // 保留类型 + static const int STAP_A = 24; // 单时间聚合包A + static const int STAP_B = 25; // 单时间聚合包B + static const int MTAP_16 = 26; // 多时间聚合包16 + static const int MTAP_24 = 27; // 多时间聚合包24 + static const int FU_A = 28; // 分片单元A + static const int FU_B = 29; // 分片单元B + static const int RESERVED_30 = 30; // 保留类型 + static const int RESERVED_31 = 31; // 保留类型 + + // 帧类型别名,方便调用 + static const int NON_IDR_PICTURE = CODED_SLICE_NON_IDR; // P帧 + static const int IDR_PICTURE = CODED_SLICE_IDR; // I帧 + + // 获取类型名称 + static String getName(int type) { + switch (type) { + case UNSPECIFIED: + return "未指定"; + case CODED_SLICE_NON_IDR: + return "P帧"; + case PARTITION_A: + return "分区A"; + case PARTITION_B: + return "分区B"; + case PARTITION_C: + return "分区C"; + case CODED_SLICE_IDR: + return "I帧"; + case SEI: + return "SEI"; + case SPS: + return "SPS"; + case PPS: + return "PPS"; + case AUD: + return "AUD"; + case END_SEQUENCE: + return "序列结束"; + case END_STREAM: + return "码流结束"; + case FILLER_DATA: + return "填充数据"; + case SPS_EXT: + return "SPS扩展"; + case PREFIX_NAL: + return "前缀NAL"; + case SUBSET_SPS: + return "子集SPS"; + case CODED_SLICE_AUX: + return "辅助切片"; + case CODED_SLICE_EXTENSION: + return "扩展切片"; + case FU_A: + return "分片单元A"; + case FU_B: + return "分片单元B"; + default: + return "未知($type)"; + } + } +} + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: '视频解码演示', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: const VideoView(), + ); + } +} + +class VideoView extends StatefulWidget { + const VideoView({Key? key}) : super(key: key); + + @override + State createState() => _VideoViewState(); +} + +/// 视频解码主页面的状态管理类。 +/// 负责: +/// 1. 加载和解析H264文件 +/// 2. 初始化、释放视频解码器 +/// 3. 控制视频播放、停止、帧解码流程 +/// 4. 管理UI刷新与日志显示 +/// 5. 维护与解码相关的核心状态(如纹理ID、播放状态、错误信息等) +/// +/// 主要成员说明: +/// - _textureId: 当前解码器绑定的纹理ID +/// - _isInitialized: 解码器是否已初始化 +/// - _isPlaying: 是否正在播放 +/// - _statusText: 当前状态文本 +/// - _error: 错误信息 +/// - _h264FileData: 加载的H264文件数据 +/// - _h264Frames: 解析出的帧列表 +/// - _currentFrameIndex: 当前播放帧索引 +/// - _frameTimer: 帧播放定时器 +/// - _logs: 日志信息 +/// - _logScrollController: 日志滚动控制器 +class _VideoViewState extends State { + // 解码器状态 + int? _textureId; + bool _isInitialized = false; + bool _isPlaying = false; + String _statusText = "未初始化"; + String _error = ""; + + // 帧统计 + int _renderedFrameCount = 0; + DateTime? _lastFrameTime; + double _fps = 0; + double _decoderFps = 0; // 解码器内部计算的FPS + + // 用于刷新解码器统计信息的定时器 + Timer? _statsTimer; + + // 丢包模拟相关 + bool _enablePacketLoss = false; + double _packetLossRate = 0.1; // 默认10%丢包率 + bool _dropIFrames = false; + bool _dropPFrames = false; + bool _dropSPSPPS = false; + bool _burstPacketLossMode = false; + int _burstPacketLossCounter = 0; + int _droppedFramesCount = 0; + + // H264文件解析 + Uint8List? _h264FileData; + List _h264Frames = []; + int _currentFrameIndex = 0; + + // 解码定时器 + Timer? _frameTimer; + + // 日志 + final List _logs = []; + final ScrollController _logScrollController = ScrollController(); + + // 视频显示相关的属性 + bool _showingErrorFrame = false; + Timer? _errorFrameResetTimer; + + // 额外的解码器属性 + int _totalFrames = 0; + int _droppedFrames = 0; + bool _hasSentIDR = false; + bool _hasSentSPS = false; + bool _hasSentPPS = false; + + @override + void initState() { + super.initState(); + _loadH264File(); + + // 仅在需要时使用定时器更新一些UI元素 + _statsTimer = Timer.periodic(Duration(milliseconds: 1000), (timer) { + if (mounted) { + setState(() { + // 更新UI元素,例如帧率计算等 + // 解码器统计信息现在通过回调获取,不需要在这里请求 + }); + } + }); + } + + @override + void dispose() { + _stopPlaying(); + _releaseDecoder(); + _frameTimer?.cancel(); + _statsTimer?.cancel(); + super.dispose(); + } + + // 加载H264文件 + Future _loadH264File() async { + try { + _log("正在加载 demo.h264 文件..."); + final ByteData data = await rootBundle.load('assets/demo.h264'); + setState(() { + _h264FileData = data.buffer.asUint8List(); + }); + _log("H264文件加载完成: ${_h264FileData!.length} 字节"); + + // 解析H264文件 + _parseH264File(); + } catch (e) { + _log("加载H264文件失败: $e"); + setState(() { + _error = "加载H264文件失败: $e"; + }); + } + } + + // 解析H264文件,提取NAL单元 + void _parseH264File() { + if (_h264FileData == null) return; + _log("开始解析H264文件..."); + List frames = []; + int startIndex = 0; + while (startIndex < _h264FileData!.length - 4) { + int nextStartIndex = _findStartCode(_h264FileData!, startIndex + 3); + if (nextStartIndex == -1) { + nextStartIndex = _h264FileData!.length; + } + int skipBytes = (_h264FileData![startIndex] == 0x00 && + _h264FileData![startIndex + 1] == 0x00 && + _h264FileData![startIndex + 2] == 0x00 && + _h264FileData![startIndex + 3] == 0x01) + ? 4 + : 3; + if (nextStartIndex > startIndex + skipBytes) { + int nalType = _h264FileData![startIndex + skipBytes] & 0x1F; + var nalData = Uint8List(nextStartIndex - startIndex); + for (int i = 0; i < nalData.length; i++) { + nalData[i] = _h264FileData![startIndex + i]; + } + // 0=I帧, 1=P帧 + if (nalType == 7 || nalType == 8 || nalType == 5) { + frames.add(H264Frame(nalData, 0)); + } else { + frames.add(H264Frame(nalData, 1)); + } + } + startIndex = nextStartIndex; + } + setState(() { + _h264Frames = frames; + }); + _log("H264文件解析完成,找到 " + frames.length.toString() + " 个帧"); + } + + // 查找起始码的辅助方法 + int _findStartCode(Uint8List data, int offset) { + for (int i = offset; i < data.length - 3; i++) { + // 检查是否为0x000001 + if (data[i] == 0x00 && data[i + 1] == 0x00 && data[i + 2] == 0x01) { + return i; + } + // 检查是否为0x00000001 + if (i < data.length - 4 && + data[i] == 0x00 && + data[i + 1] == 0x00 && + data[i + 2] == 0x00 && + data[i + 3] == 0x01) { + return i; + } + } + return -1; + } + + // 获取NAL类型 + int _getNalType(Uint8List data) { + // 跳过起始码后再获取NAL单元类型 + if (data.length > 4) { + // 检查是否有0x00000001的起始码 + if (data[0] == 0x00 && + data[1] == 0x00 && + data[2] == 0x00 && + data[3] == 0x01) { + // 起始码后的第一个字节的低5位是NAL类型 + if (data.length > 4) { + return data[4] & 0x1F; + } + } + // 检查是否有0x000001的起始码 + else if (data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01) { + // 起始码后的第一个字节的低5位是NAL类型 + if (data.length > 3) { + return data[3] & 0x1F; + } + } + } + + // 如果找不到起始码或数据不足,则尝试直接获取 + if (data.length > 0) { + return data[0] & 0x1F; + } + return 0; + } + + // 当新帧可用时调用 + void _onFrameAvailable(int textureId) { + if (!mounted) return; + + _log("收到帧回调 - 渲染帧 ${_renderedFrameCount + 1}"); + + // 更新帧时间用于FPS计算 + _lastFrameTime = DateTime.now(); + + // 立即更新UI以显示新帧 + setState(() { + _renderedFrameCount++; + }); + } + + Future _initializeDecoder() async { + if (_isInitialized) { + await _releaseDecoder(); + } + _log("正在初始化解码器"); + try { + final config = VideoDecoderConfig(width: 640, height: 480); + final textureId = await VideoDecodePlugin.initDecoder(config); + if (textureId != null) { + _textureId = textureId; + setState(() { + _isInitialized = true; + _error = ""; + _statusText = "就绪"; + _renderedFrameCount = 0; + }); + _log("解码器初始化成功,纹理ID: $_textureId"); + } else { + setState(() { + _error = "获取纹理ID失败"; + _statusText = "初始化失败"; + }); + _log("解码器初始化失败 - 返回空纹理ID"); + } + } catch (e) { + setState(() { + _error = e.toString(); + _statusText = "初始化错误"; + }); + _log("解码器初始化错误: $e"); + } + } + + // 解码帧(已适配sendFrame,splitNalFromIFrame=false) + Future _decodeNextFrame(H264Frame frame, int frameSeq) async { + if (_textureId == null || !_isInitialized || !_isPlaying) { + return; + } + try { + final timestamp = DateTime.now().microsecondsSinceEpoch; + // 如果是I帧,先发送SPS和PPS + int nalType = _getNalType(frame.data); + if (nalType == NalUnitType.CODED_SLICE_IDR) { + // 查找最近的SPS和PPS + H264Frame? sps, pps; + for (int i = frameSeq - 1; i >= 0; i--) { + int t = _getNalType(_h264Frames[i].data); + if (sps == null && t == NalUnitType.SPS) sps = _h264Frames[i]; + if (pps == null && t == NalUnitType.PPS) pps = _h264Frames[i]; + if (sps != null && pps != null) break; + } + if (sps != null) { + await VideoDecodePlugin.sendFrame( + frameData: sps.data, + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq - 2, + splitNalFromIFrame: false, + ); + } + if (pps != null) { + await VideoDecodePlugin.sendFrame( + frameData: pps.data, + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq - 1, + splitNalFromIFrame: false, + ); + } + } + await VideoDecodePlugin.sendFrame( + frameData: frame.data, + frameType: frame.frameType, + timestamp: timestamp, + frameSeq: frameSeq, + splitNalFromIFrame: false, + ); + } catch (e) { + _log("解码帧错误: $e"); + } + } + + Future _releaseDecoder() async { + if (_textureId != null) { + _log("正在释放解码器资源"); + try { + await VideoDecodePlugin.releaseDecoder(); + setState(() { + _textureId = null; + _isInitialized = false; + _statusText = "已释放"; + }); + _log("解码器资源释放成功"); + } catch (e) { + _log("释放解码器错误: $e"); + } + } + } + + Future _startPlaying() async { + if (!_isInitialized || _isPlaying || _h264Frames.isEmpty) { + _log( + "播放条件未满足: 初始化=${_isInitialized}, 播放中=${_isPlaying}, 帧数量=${_h264Frames.length}"); + return; + } + + _log("开始播放H264视频"); + + try { + // 重置帧率跟踪 + _renderedFrameCount = 0; + _lastFrameTime = null; + _fps = 0; + _currentFrameIndex = 0; + + // 确保首先发送SPS和PPS + await _sendSpsAndPps(); + + // 开始解码帧 + _startDecodingFrames(); + + setState(() { + _isPlaying = true; + _statusText = "播放中"; + }); + + _log("播放已开始"); + } catch (e) { + _log("播放开始错误: $e"); + } + } + + // 发送SPS和PPS + Future _sendSpsAndPps() async { + for (int i = 0; i < math.min(10, _h264Frames.length); i++) { + H264Frame frame = _h264Frames[i]; + + // 检查是否是SPS或PPS (通过检查NAL类型) + if (frame.data.length > 4) { + int skipBytes = (frame.data[0] == 0x00 && + frame.data[1] == 0x00 && + frame.data[2] == 0x00 && + frame.data[3] == 0x01) + ? 4 + : 3; + + if (skipBytes < frame.data.length) { + int nalType = frame.data[skipBytes] & 0x1F; + + if (nalType == NalUnitType.SPS || nalType == NalUnitType.PPS) { + _log("发送${nalType == NalUnitType.SPS ? 'SPS' : 'PPS'}数据"); + await _decodeNextFrame(frame, i); + // 发送后等待一小段时间,确保解码器处理 + await Future.delayed(Duration(milliseconds: 30)); + } + } + } + } + } + + void _stopPlaying() { + if (!_isPlaying) return; + + _log("停止播放"); + + _frameTimer?.cancel(); + _frameTimer = null; + + setState(() { + _isPlaying = false; + _statusText = "已停止"; + }); + + _log("播放已停止"); + } + + void _startDecodingFrames() { + _log("开始解码视频帧"); + + // 使用与原视频接近的帧率 + const int frameIntervalMs = 42; // 约23.8 fps (接近原视频23.9fps) + + _frameTimer = + Timer.periodic(Duration(milliseconds: frameIntervalMs), (timer) async { + if (_currentFrameIndex >= _h264Frames.length) { + _log("所有帧已解码,重新开始"); + _currentFrameIndex = 0; + } + final frame = _h264Frames[_currentFrameIndex]; + // 未初始化成功时不发送解码帧 + if (!_isInitialized || _textureId == null) return; + await _decodeNextFrame(frame, _currentFrameIndex); + _currentFrameIndex++; + }); + } + + void _log(String message) { + final timestamp = DateTime.now().toString().split('.').first; + final logMessage = "[$timestamp] $message"; + + setState(() { + _logs.add(logMessage); + if (_logs.length > 100) { + _logs.removeAt(0); + } + }); + + // 滚动到底部 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (_logScrollController.hasClients) { + _logScrollController.animateTo( + _logScrollController.position.maxScrollExtent, + duration: Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + } + }); + } + + Widget _buildVideoDisplay() { + if (_textureId == null) { + return Container( + width: 640, + height: 480, + color: Colors.black54, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.videocam_off, size: 48, color: Colors.white70), + SizedBox(height: 16), + Text( + '无可用纹理', + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ], + ), + ), + ); + } + + return Stack( + fit: StackFit.expand, + children: [ + // 背景色 + Container(color: Colors.black), + + // 无帧时显示加载指示 + if (_renderedFrameCount == 0) + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white70), + ), + SizedBox(height: 16), + Text( + '初始化中...', + style: TextStyle(color: Colors.white70, fontSize: 14), + ), + ], + ), + ), + + // 视频纹理 - 使用RepaintBoundary和ValueKey确保正确更新 + RepaintBoundary( + child: Texture( + textureId: _textureId!, + filterQuality: FilterQuality.medium, + key: ValueKey('texture_${_renderedFrameCount}'), + ), + ), + + // 丢包效果指示器 - 当有帧被丢弃时,上方显示红色条带 + if (_enablePacketLoss && _droppedFramesCount > 0) + Positioned( + top: 0, + left: 0, + right: 0, + height: 5, + child: Container( + color: Colors.red.withOpacity(0.8), + ), + ), + + // // 显示帧计数 - 调试用 + // Positioned( + // right: 10, + // top: 10, + // child: Container( + // padding: EdgeInsets.all(5), + // decoration: BoxDecoration( + // color: Colors.black.withOpacity(0.5), + // borderRadius: BorderRadius.circular(4), + // ), + // constraints: BoxConstraints( + // maxWidth: 150, // 限制最大宽度 + // ), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.end, + // mainAxisSize: MainAxisSize.min, // 确保column只占用所需空间 + // children: [ + // Text( + // '帧: $_renderedFrameCount', + // style: TextStyle(color: Colors.white, fontSize: 12), + // ), + // if (_enablePacketLoss) + // Text( + // '丢帧: $_droppedFramesCount', + // style: TextStyle( + // color: _droppedFramesCount > 0 + // ? Colors.orange + // : Colors.white70, + // fontSize: 12, + // ), + // ), + // ], + // ), + // ), + // ), + ], + ); + } + + @override + Widget build(BuildContext context) { + // 更新FPS计算 + if (_lastFrameTime != null && _renderedFrameCount > 0) { + final now = DateTime.now(); + final elapsed = now.difference(_lastFrameTime!).inMilliseconds; + if (elapsed > 0) { + _fps = 1000 / elapsed; + } + } + + return Scaffold( + appBar: AppBar( + title: Text('视频解码插件演示'), + backgroundColor: Theme.of(context).colorScheme.primaryContainer, + ), + body: SafeArea( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 视频显示区域 - 使用AspectRatio控制大小 + AspectRatio( + aspectRatio: 16 / 9, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.grey), + ), + child: _buildVideoDisplay(), + ), + ), + + // 控制面板和状态信息 - 使用Expanded和SingleChildScrollView + Expanded( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 状态信息区 - 使用Card使其更紧凑 + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 状态行 + Text('状态: $_statusText', + style: + TextStyle(fontWeight: FontWeight.bold)), + + // FPS和帧数信息 + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text( + // '解码器FPS: ${_decoderFps.toStringAsFixed(1)}', + // style: TextStyle(color: Colors.green), + // ), + // ), + // + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text('已渲染帧数: $_renderedFrameCount'), + // ), + // + // // 丢弃帧信息 + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text( + // '已丢弃帧数: $_droppedFramesCount', + // style: TextStyle( + // color: _droppedFramesCount > 0 + // ? Colors.orange + // : Colors.black), + // ), + // ), + // + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text('当前帧索引: $_currentFrameIndex'), + // ), + // + // // 参数集状态 + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Text('参数集状态:'), + // Padding( + // padding: const EdgeInsets.only( + // left: 8.0, top: 2.0), + // child: Text( + // 'SPS: ${_hasSentSPS ? "已发送" : "未发送"}'), + // ), + // Padding( + // padding: const EdgeInsets.only( + // left: 8.0, top: 2.0), + // child: Text( + // 'PPS: ${_hasSentPPS ? "已发送" : "未发送"}'), + // ), + // Padding( + // padding: const EdgeInsets.only( + // left: 8.0, top: 2.0), + // child: Text( + // 'IDR: ${_hasSentIDR ? "已发送" : "未发送"}'), + // ), + // ], + // ), + // ), + // + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text('总帧数: $_totalFrames'), + // ), + // + // // 错误信息 + // if (_error.isNotEmpty) + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text( + // '错误: $_error', + // style: TextStyle( + // color: Colors.red, + // fontWeight: FontWeight.bold), + // ), + // ), + + // H264文件信息 + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text('解析的帧数: ${_h264Frames.length}'), + ), + + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + 'H264文件大小: ${((_h264FileData?.length ?? 0) ~/ 1024)} KB'), + ), + ], + ), + ), + ), + + // 控制按钮区 + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: [ + ElevatedButton( + onPressed: + _isInitialized ? null : _initializeDecoder, + child: Text('初始化'), + ), + ElevatedButton( + onPressed: (!_isInitialized || + _isPlaying || + _h264Frames.isEmpty) + ? null + : _startPlaying, + child: Text('播放'), + ), + ElevatedButton( + onPressed: (!_isPlaying) ? null : _stopPlaying, + child: Text('停止'), + ), + ElevatedButton( + onPressed: (_textureId == null) + ? null + : _releaseDecoder, + child: Text('释放'), + ), + ElevatedButton( + onPressed: () { + setState(() { + // 强制重绘 + _renderedFrameCount++; + }); + }, + child: Text('刷新'), + ), + ], + ), + ), + ), + + // 丢包模拟控制面板 + Card( + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: + MainAxisAlignment.spaceBetween, + children: [ + Text('丢包模拟', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 16)), + Switch( + value: _enablePacketLoss, + onChanged: (value) { + setState(() { + _enablePacketLoss = value; + if (value) { + _droppedFramesCount = 0; // 重置丢帧计数 + } + }); + }, + ), + ], + ), + + // 丢包率控制 + Row( + children: [ + Text( + '丢包率: ${(_packetLossRate * 100).toStringAsFixed(0)}%'), + Expanded( + child: Slider( + value: _packetLossRate, + min: 0.0, + max: 1.0, + divisions: 20, + onChanged: _enablePacketLoss + ? (value) { + setState(() { + _packetLossRate = value; + }); + } + : null, + ), + ), + ], + ), + + // 丢包模式选择 + Row( + children: [ + Expanded( + child: CheckboxListTile( + dense: true, + title: Text('爆发式丢包'), + value: _burstPacketLossMode, + onChanged: _enablePacketLoss + ? (value) { + setState(() { + _burstPacketLossMode = value!; + }); + } + : null, + ), + ), + Expanded( + child: CheckboxListTile( + dense: true, + title: Text('丢弃I帧'), + value: _dropIFrames, + onChanged: _enablePacketLoss + ? (value) { + setState(() { + _dropIFrames = value!; + }); + } + : null, + ), + ), + ], + ), + + Row( + children: [ + Expanded( + child: CheckboxListTile( + dense: true, + title: Text('丢弃P帧'), + value: _dropPFrames, + onChanged: _enablePacketLoss + ? (value) { + setState(() { + _dropPFrames = value!; + }); + } + : null, + ), + ), + Expanded( + child: CheckboxListTile( + dense: true, + title: Text('丢弃SPS/PPS'), + value: _dropSPSPPS, + onChanged: _enablePacketLoss + ? (value) { + setState(() { + _dropSPSPPS = value!; + }); + } + : null, + ), + ), + ], + ), + + // 丢包统计信息 + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '已丢弃帧数: $_droppedFramesCount', + style: TextStyle( + color: _droppedFramesCount > 0 + ? Colors.red + : Colors.black), + ), + ), + ], + ), + ), + ), + + // 日志区域 + SizedBox(height: 8), + Text('日志:', + style: TextStyle(fontWeight: FontWeight.bold)), + SizedBox(height: 4), + Container( + height: 280, + decoration: BoxDecoration( + color: Colors.black, + border: Border.all(color: Colors.grey), + borderRadius: BorderRadius.circular(4.0), + ), + padding: EdgeInsets.all(4), + child: ListView.builder( + controller: _logScrollController, + itemCount: _logs.length, + itemBuilder: (context, index) { + return Text( + _logs[index], + style: TextStyle( + color: _logs[index].contains('错误') + ? Colors.red + : Colors.green, + fontSize: 11, + fontFamily: 'monospace', + ), + ); + }, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } +} + +// 添加错误帧绘制器 +class ErrorFramePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final random = math.Random(); + final paint = Paint() + ..color = Colors.red.withOpacity(0.2) + ..style = PaintingStyle.fill; + + // 绘制随机条纹效果 + for (int i = 0; i < 20; i++) { + double y = random.nextDouble() * size.height; + canvas.drawRect( + Rect.fromLTWH(0, y, size.width, 3 + random.nextDouble() * 5), + paint, + ); + } + + // 绘制马赛克块 + for (int i = 0; i < 50; i++) { + double x = random.nextDouble() * size.width; + double y = random.nextDouble() * size.height; + double blockSize = 10 + random.nextDouble() * 30; + + canvas.drawRect( + Rect.fromLTWH(x, y, blockSize, blockSize), + Paint() + ..color = Color.fromRGBO(random.nextInt(255), random.nextInt(255), + random.nextInt(255), 0.3)); + } + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return true; // 每次都重新绘制以产生动态效果 + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..28ec027 --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,387 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: b543301ad291598523947dc534aaddc5aaad597b709d2426d3a0e0d44c5cb493 + url: "https://pub.dev" + source: hosted + version: "1.0.4" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: a2c3d198cb5ea2e179926622d433331d8b58374ab8f29cdda6e863bd62fd369c + url: "https://pub.dev" + source: hosted + version: "1.0.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" + source: hosted + version: "0.8.0" + meta: + dependency: transitive + description: + name: meta + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + url: "https://pub.dev" + source: hosted + version: "1.11.0" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 + url: "https://pub.dev" + source: hosted + version: "2.1.4" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: a248d8146ee5983446bf03ed5ea8f6533129a12b11f12057ad1b4a67a2b3b41d + url: "https://pub.dev" + source: hosted + version: "2.2.4" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + url: "https://pub.dev" + source: hosted + version: "10.4.5" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + url: "https://pub.dev" + source: hosted + version: "10.3.6" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + url: "https://pub.dev" + source: hosted + version: "9.1.4" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + url: "https://pub.dev" + source: hosted + version: "3.12.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + url: "https://pub.dev" + source: hosted + version: "0.1.3" + platform: + dependency: transitive + description: + name: platform + sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + url: "https://pub.dev" + source: hosted + version: "3.1.4" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + url: "https://pub.dev" + source: hosted + version: "0.6.1" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + video_decode_plugin: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + url: "https://pub.dev" + source: hosted + version: "3.0.3" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" +sdks: + dart: ">=3.3.4 <4.0.0" + flutter: ">=3.19.0" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..87c4e5a --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,91 @@ +name: video_decode_plugin_example +description: "Demonstrates how to use the video_decode_plugin plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ">=2.16.1 <3.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + video_decode_plugin: + # When depending on this package from a real application you should use: + # video_decode_plugin: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + path_provider: ^2.0.9 + permission_handler: ^10.0.0 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^1.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # 添加assets资源 + assets: + - assets/demo.h264 + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/ios/.gitignore b/ios/.gitignore new file mode 100644 index 0000000..0c88507 --- /dev/null +++ b/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/ios/Assets/.gitkeep b/ios/Assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/ios/Classes/VideoDecodePlugin.swift b/ios/Classes/VideoDecodePlugin.swift new file mode 100644 index 0000000..e503c14 --- /dev/null +++ b/ios/Classes/VideoDecodePlugin.swift @@ -0,0 +1,209 @@ +import Flutter +import UIKit +import AVFoundation + +public class VideoDecodePlugin: NSObject, FlutterPlugin, FlutterTexture { + private var channel: FlutterMethodChannel? + private var registrar: FlutterPluginRegistrar? + private var decoder: VideoDecoder? + private var textureId: Int64? + private var textureRegistry: FlutterTextureRegistry? + private var latestPixelBuffer: CVPixelBuffer? + private let textureQueue = DispatchQueue(label: "video_decode_plugin.texture.queue") + private var cachedSps: Data? + private var cachedPps: Data? + private var hasNotifiedFlutter = false + + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "video_decode_plugin", binaryMessenger: registrar.messenger()) + let instance = VideoDecodePlugin() + instance.channel = channel + instance.registrar = registrar + instance.textureRegistry = registrar.textures() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "initDecoder": + handleInitDecoder(call: call, result: result) + case "decodeFrame": + handleDecodeFrame(call: call, result: result) + case "releaseDecoder": + handleReleaseDecoder(call: call, result: result) + default: + result(FlutterMethodNotImplemented) + } + } + + /// 初始化解码器 + private func handleInitDecoder(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let width = args["width"] as? Int, + let height = args["height"] as? Int, + let codecType = args["codecType"] as? String else { + result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil)) + return + } + // 释放旧解码器和纹理 + decoder?.release() + decoder = nil + if let tid = textureId { + textureRegistry?.unregisterTexture(tid) + textureId = nil + } + // 注册Flutter纹理 + guard let registry = textureRegistry else { + result(FlutterError(code: "NO_TEXTURE_REGISTRY", message: "无法获取纹理注册表", details: nil)) + return + } + let textureId = registry.register(self) + self.textureId = textureId + // 创建解码器 + let decoder = VideoDecoder(width: width, height: height, codecType: codecType) + self.decoder = decoder + decoder.onFrameDecoded = { [weak self] pixelBuffer, _ in + guard let self = self else { return } + self.textureQueue.async { + self.latestPixelBuffer = pixelBuffer + self.textureRegistry?.textureFrameAvailable(self.textureId ?? 0) + if !self.hasNotifiedFlutter { + self.hasNotifiedFlutter = true + DispatchQueue.main.async { + self.channel?.invokeMethod("onFrameRendered", arguments: ["textureId": self.textureId ?? 0]) + } + } + } + } + print("[VideoDecodePlugin] 解码器初始化成功,textureId=\(textureId)") + result(textureId) + } + + /// 新增:去除NALU起始码的工具方法(增强日志与健壮性) + private func stripStartCode(_ data: Data) -> Data { + let originalLen = data.count + let naluType: UInt8 = { + if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 { + return data[4] & 0x1F + } else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 { + return data[3] & 0x1F + } + return 0 + }() + var stripped: Data = data + if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 { + stripped = data.subdata(in: 4.. 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 { + stripped = data.subdata(in: 3.. 0 ? (stripped[0] & 0x1F) : 0 + if strippedLen < 3 || (strippedType != 7 && strippedType != 8) { + print("[VideoDecodePlugin][警告] strip后NALU长度或类型异常,type=", strippedType, "len=", strippedLen) + } + // 只在异常时输出警告,不再输出详细内容 + return stripped + } + + /// 解码视频帧 + private func handleDecodeFrame(call: FlutterMethodCall, result: @escaping FlutterResult) { + guard let args = call.arguments as? [String: Any], + let frameData = args["frameData"] as? FlutterStandardTypedData, + let frameType = args["frameType"] as? Int, + let timestamp = args["timestamp"] as? Int, + let frameSeq = args["frameSeq"] as? Int else { + result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil)) + return + } + let refIFrameSeq = args["refIFrameSeq"] as? Int + let data = frameData.data + + // 解析NALU类型 + let naluType: UInt8 = { + if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 { + return data[4] & 0x1F + } else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 { + return data[3] & 0x1F + } + return 0 + }() + + print("[VideoDecodePlugin][调试] handleDecodeFrame: frameType=\(frameType), naluType=\(naluType), cachedSpsLen=\(cachedSps?.count ?? 0), cachedPpsLen=\(cachedPps?.count ?? 0)") + + // 缓存SPS/PPS(去除起始码)并立即尝试初始化解码器 + if naluType == 7 { // SPS + // 只缓存,不再输出详细日志 + cachedSps = stripStartCode(data) + result(true) + return + } else if naluType == 8 { // PPS + // 只缓存,不再输出详细日志 + cachedPps = stripStartCode(data) + result(true) + return + } else if naluType == 5 { // IDR/I帧 + // 先提取第一个合法NALU,直接推送 + let firstNalu = extractFirstValidNalu(data) + if firstNalu.isEmpty { return } + print("[VideoDecodePlugin] 发送I帧, 长度: \(firstNalu.count), 头部: \(firstNalu.prefix(8).map { String(format: "%02X", $0) }.joined(separator: " ")), cachedSps长度: \(cachedSps?.count ?? 0), cachedPps长度: \(cachedPps?.count ?? 0)") + decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq, sps: cachedSps, pps: cachedPps) + } else { + // 先提取第一个合法NALU,直接推送 + let firstNalu = extractFirstValidNalu(data) + if firstNalu.isEmpty { return } + print("[VideoDecodePlugin] 发送P/B帧, 长度: \(firstNalu.count), 头部: \(firstNalu.prefix(8).map { String(format: "%02X", $0) }.joined(separator: " "))") + decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq) + } + result(true) + } + + /// 释放解码器资源 + private func handleReleaseDecoder(call: FlutterMethodCall, result: @escaping FlutterResult) { + decoder?.release() + decoder = nil + if let tid = textureId { + textureRegistry?.unregisterTexture(tid) + textureId = nil + } + latestPixelBuffer = nil + hasNotifiedFlutter = false + print("[VideoDecodePlugin] 解码器和纹理已释放") + result(true) + } + + // MARK: - FlutterTexture协议实现 + public func copyPixelBuffer() -> Unmanaged? { + var pixelBuffer: CVPixelBuffer? + textureQueue.sync { + pixelBuffer = self.latestPixelBuffer + } + if let pb = pixelBuffer { + return Unmanaged.passRetained(pb) + } + return nil + } + + // 新增side_data检测工具 + private func checkNaluForSideData(_ nalu: Data, naluType: UInt8) -> Bool { + if (naluType == 5 && nalu.count > 10000) || (naluType != 7 && naluType != 8 && nalu.count > 10000) { + print("[VideoDecodePlugin][警告] NALU长度异常,可能包含side_data,type=\(naluType),len=\(nalu.count)") + return true + } + return false + } + + // 新增:提取第一个合法NALU工具 + private func extractFirstValidNalu(_ nalu: Data) -> Data { + guard let start = nalu.range(of: Data([0x00, 0x00, 0x00, 0x01]))?.lowerBound else { + print("[VideoDecodePlugin][警告] NALU无AnnexB起始码,丢弃该帧") + return Data() + } + let searchRange = (start+4)..() + /// 最大允许延迟(毫秒) + private let maxAllowedDelayMs: Int = 350 + /// 时间戳基准 + private var timestampBaseMs: Int64? + /// 首帧相对时间戳 + private var firstFrameRelativeTimestamp: Int64? + + // ====== 新增:缓冲区与自适应帧率相关成员 ====== + /// 输入缓冲区(待解码帧队列),线程安全 + private let inputQueue = DispatchQueue(label: "video_decode_plugin.input.queue", attributes: .concurrent) + private var inputBuffer: [(frameData: Data, frameType: Int, timestamp: Int64, frameSeq: Int, refIFrameSeq: Int?, sps: Data?, pps: Data?)] = [] + private let inputBufferSemaphore = DispatchSemaphore(value: 1) + private let inputBufferMaxCount = 30 + /// 输出缓冲区(解码后帧队列),线程安全 + private let outputQueue = DispatchQueue(label: "video_decode_plugin.output.queue", attributes: .concurrent) + private var outputBuffer: [(pixelBuffer: CVPixelBuffer, timestamp: Int64)] = [] + private let outputBufferSemaphore = DispatchSemaphore(value: 1) + private let outputBufferMaxCount = 20 + /// 渲染线程 + private var renderThread: Thread? + /// 渲染线程运行标志 + private var renderThreadRunning = false + /// 首次渲染回调标志 + private var hasNotifiedFlutter = false + /// 当前渲染帧率 + private var renderFps: Int = 15 + /// EMA平滑后的帧率 + private var smoothedFps: Double = 15.0 + /// EMA平滑系数 + private let alpha: Double = 0.2 + /// 最小帧率 + private let minFps: Double = 8.0 + /// 最大帧率 + private let maxFps: Double = 30.0 + /// 单次最大调整幅度 + private let maxStep: Double = 2.0 + /// 渲染帧时间戳队列 + private var renderedTimestamps: [Int64] = [] // ms + /// 渲染帧时间戳最大数量 + private let renderedTimestampsMaxCount = 20 + /// 已渲染帧计数 + private var renderedFrameCount = 0 + /// 每N帧调整一次帧率 + private let fpsAdjustInterval = 10 + + /// 解码回调,输出CVPixelBuffer和时间戳 + var onFrameDecoded: ((CVPixelBuffer, Int64) -> Void)? = { _, _ in } + + /// 初始化解码器,启动渲染线程 + init(width: Int, height: Int, codecType: String) { + self.width = width + self.height = height + self.codecType = CodecType(rawValue: codecType.lowercased()) ?? .h264 + startRenderThread() + } + + // ====== 输入缓冲区操作 ====== + /// 入队待解码帧 + private func enqueueInput(_ item: (Data, Int, Int64, Int, Int?, Data?, Data?)) { + inputQueue.async(flags: .barrier) { + if self.inputBuffer.count >= self.inputBufferMaxCount { + self.inputBuffer.removeFirst() + } + self.inputBuffer.append(item) + } + } + /// 出队待解码帧 + private func dequeueInput() -> (Data, Int, Int64, Int, Int?, Data?, Data?)? { + var item: (Data, Int, Int64, Int, Int?, Data?, Data?)? + inputQueue.sync { + if !self.inputBuffer.isEmpty { + item = self.inputBuffer.removeFirst() + } + } + return item + } + // ====== 输出缓冲区操作 ====== + /// 入队解码后帧 + private func enqueueOutput(_ item: (CVPixelBuffer, Int64)) { + outputQueue.async(flags: .barrier) { + if self.outputBuffer.count >= self.outputBufferMaxCount { + self.outputBuffer.removeFirst() + } + self.outputBuffer.append(item) + } + } + /// 出队解码后帧 + private func dequeueOutput() -> (CVPixelBuffer, Int64)? { + var item: (CVPixelBuffer, Int64)? + outputQueue.sync { + if !self.outputBuffer.isEmpty { + item = self.outputBuffer.removeFirst() + } + } + return item + } + // ====== 渲染线程相关 ====== + /// 启动渲染线程,定时从输出缓冲区取帧并刷新Flutter纹理,支持EMA自适应帧率 + private func startRenderThread() { + renderThreadRunning = true + renderThread = Thread { [weak self] in + guard let self = self else { return } + while self.renderThreadRunning { + let frameIntervalMs = Int(1000.0 / self.smoothedFps) + let loopStart = Date().timeIntervalSince1970 * 1000.0 + if let (pixelBuffer, timestamp) = self.dequeueOutput() { + // 渲染到Flutter纹理 + DispatchQueue.main.async { + self.onFrameDecoded?(pixelBuffer, timestamp) + } + // 只在首次渲染时回调Flutter + if !self.hasNotifiedFlutter { + self.hasNotifiedFlutter = true + // 由外部插件层负责onFrameRendered回调 + } + // 帧率统计 + self.renderedTimestamps.append(Int64(Date().timeIntervalSince1970 * 1000)) + if self.renderedTimestamps.count > self.renderedTimestampsMaxCount { + self.renderedTimestamps.removeFirst() + } + self.renderedFrameCount += 1 + if self.renderedFrameCount % self.fpsAdjustInterval == 0 { + let measuredFps = self.calculateDecodeFps() + let newFps = self.updateSmoothedFps(measuredFps) + self.renderFps = newFps + } + } + // 控制渲染节奏 + let loopCost = Int(Date().timeIntervalSince1970 * 1000.0 - loopStart) + let sleepMs = frameIntervalMs - loopCost + if sleepMs > 0 { + Thread.sleep(forTimeInterval: Double(sleepMs) / 1000.0) + } + } + } + renderThread?.start() + } + /// 停止渲染线程 + private func stopRenderThread() { + renderThreadRunning = false + renderThread?.cancel() + renderThread = nil + } + // ====== EMA帧率平滑算法 ====== + /// 计算最近N帧的平均解码帧率 + private func calculateDecodeFps() -> Double { + guard renderedTimestamps.count >= 2 else { return smoothedFps } + let first = renderedTimestamps.first! + let last = renderedTimestamps.last! + let frameCount = renderedTimestamps.count - 1 + let durationMs = max(last - first, 1) + return Double(frameCount) * 1000.0 / Double(durationMs) + } + /// EMA平滑更新渲染帧率 + private func updateSmoothedFps(_ measuredFps: Double) -> Int { + let safeFps = min(max(measuredFps, minFps), maxFps) + let targetFps = alpha * safeFps + (1 - alpha) * smoothedFps + let delta = targetFps - smoothedFps + let step = min(max(delta, -maxStep), maxStep) + smoothedFps = min(max(smoothedFps + step, minFps), maxFps) + return Int(smoothedFps) + } + /// 初始化解码会话(首次收到I帧时调用) + private func setupSession(sps: Data?, pps: Data?) -> Bool { + // 释放旧会话 + if let session = decompressionSession { + VTDecompressionSessionInvalidate(session) + decompressionSession = nil + } + formatDesc = nil + isSessionReady = false + guard let sps = sps, let pps = pps else { + print("[VideoDecoder] 缺少SPS/PPS,无法初始化解码会话") + return false + } + // 校验SPS/PPS长度和类型 + let spsType: UInt8 = sps.count > 0 ? (sps[0] & 0x1F) : 0 + let ppsType: UInt8 = pps.count > 0 ? (pps[0] & 0x1F) : 0 + if sps.count < 3 || spsType != 7 { + print("[VideoDecoder][错误] SPS内容异常,len=\(sps.count), type=\(spsType)") + return false + } + if pps.count < 3 || ppsType != 8 { + print("[VideoDecoder][错误] PPS内容异常,len=\(pps.count), type=\(ppsType)") + return false + } + var success = false + sps.withUnsafeBytes { spsPtr in + pps.withUnsafeBytes { ppsPtr in + let parameterSetPointers: [UnsafePointer] = [ + spsPtr.baseAddress!.assumingMemoryBound(to: UInt8.self), + ppsPtr.baseAddress!.assumingMemoryBound(to: UInt8.self) + ] + let parameterSetSizes: [Int] = [sps.count, pps.count] + let status = CMVideoFormatDescriptionCreateFromH264ParameterSets( + allocator: kCFAllocatorDefault, + parameterSetCount: 2, + parameterSetPointers: parameterSetPointers, + parameterSetSizes: parameterSetSizes, + nalUnitHeaderLength: 4, + formatDescriptionOut: &formatDesc + ) + if status != noErr { + print("[VideoDecoder] 创建FormatDescription失败: \(status)") + success = false + } else { + success = true + } + } + } + if !success { return false } + var callback = VTDecompressionOutputCallbackRecord( + decompressionOutputCallback: { (decompressionOutputRefCon, _, status, _, imageBuffer, pts, _) in + let decoder = Unmanaged.fromOpaque(decompressionOutputRefCon!).takeUnretainedValue() + if status == noErr, let pixelBuffer = imageBuffer { + // 入队到输出缓冲区,由渲染线程拉取 + decoder.enqueueOutput((pixelBuffer, Int64(pts.seconds * 1000))) + } else { + print("[VideoDecoder] 解码回调失败, status=\(status)") + } + }, + decompressionOutputRefCon: UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque()) + ) + let attrs: [NSString: Any] = [ + kCVPixelBufferPixelFormatTypeKey: kCVPixelFormatType_420YpCbCr8BiPlanarFullRange, + kCVPixelBufferWidthKey: width, + kCVPixelBufferHeightKey: height, + kCVPixelBufferOpenGLESCompatibilityKey: true + ] + let status2 = VTDecompressionSessionCreate( + allocator: kCFAllocatorDefault, + formatDescription: formatDesc!, + decoderSpecification: nil, + imageBufferAttributes: attrs as CFDictionary, + outputCallback: &callback, + decompressionSessionOut: &decompressionSession + ) + if status2 != noErr { + print("[VideoDecoder] 创建解码会话失败: \(status2)") + return false + } + isSessionReady = true + print("[VideoDecoder] 解码会话初始化成功") + return true + } + + /// 解码一帧数据 + func decodeFrame(frameData: Data, frameType: Int, timestamp: Int64, frameSeq: Int, refIFrameSeq: Int?, sps: Data? = nil, pps: Data? = nil) { + enqueueInput((frameData, frameType, timestamp, frameSeq, refIFrameSeq, sps, pps)) + // 解码线程异步处理 + decodeQueue.async { [weak self] in + guard let self = self else { return } + guard let (frameData, frameType, timestamp, frameSeq, refIFrameSeq, sps, pps) = self.dequeueInput() else { return } + if !self.isSessionReady, let sps = sps, let pps = pps { + guard self.setupSession(sps: sps, pps: pps) else { return } + } + guard let session = self.decompressionSession else { return } + guard frameData.count > 4 else { return } + var avccData = frameData + let naluLen = UInt32(frameData.count - 4).bigEndian + if avccData.count >= 4 { + avccData.replaceSubrange(0..<4, with: withUnsafeBytes(of: naluLen) { Data($0) }) + } else { + return + } + var blockBuffer: CMBlockBuffer? + let status = CMBlockBufferCreateWithMemoryBlock( + allocator: kCFAllocatorDefault, + memoryBlock: UnsafeMutableRawPointer(mutating: (avccData as NSData).bytes), + blockLength: avccData.count, + blockAllocator: kCFAllocatorNull, + customBlockSource: nil, + offsetToData: 0, + dataLength: avccData.count, + flags: 0, + blockBufferOut: &blockBuffer + ) + if status != kCMBlockBufferNoErr { return } + var sampleBuffer: CMSampleBuffer? + var timing = CMSampleTimingInfo(duration: .invalid, presentationTimeStamp: CMTime(value: timestamp, timescale: 1000), decodeTimeStamp: .invalid) + let status2 = CMSampleBufferCreate( + allocator: kCFAllocatorDefault, + dataBuffer: blockBuffer, + dataReady: true, + makeDataReadyCallback: nil, + refcon: nil, + formatDescription: self.formatDesc, + sampleCount: 1, + sampleTimingEntryCount: 1, + sampleTimingArray: &timing, + sampleSizeEntryCount: 1, + sampleSizeArray: [avccData.count], + sampleBufferOut: &sampleBuffer + ) + if status2 != noErr { return } + let decodeFlags: VTDecodeFrameFlags = [] + var infoFlags = VTDecodeInfoFlags() + let status3 = VTDecompressionSessionDecodeFrame( + session, + sampleBuffer: sampleBuffer!, + flags: decodeFlags, + frameRefcon: nil, + infoFlagsOut: &infoFlags + ) + if status3 != noErr { + print("[VideoDecoder] 解码失败: \(status3)") + } + } + } + + /// 释放解码器资源 + func release() { + stopRenderThread() + decodeQueue.sync { + if let session = decompressionSession { + VTDecompressionSessionInvalidate(session) + } + decompressionSession = nil + formatDesc = nil + isSessionReady = false + frameSeqSet.removeAll() + lastIFrameSeq = nil + } + inputQueue.async(flags: .barrier) { self.inputBuffer.removeAll() } + outputQueue.async(flags: .barrier) { self.outputBuffer.removeAll() } + print("[VideoDecoder] 解码器已释放") + } +} \ No newline at end of file diff --git a/ios/video_decode_plugin.podspec b/ios/video_decode_plugin.podspec new file mode 100644 index 0000000..b4e02bc --- /dev/null +++ b/ios/video_decode_plugin.podspec @@ -0,0 +1,23 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint video_decode_plugin.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'video_decode_plugin' + s.version = '0.0.1' + s.summary = 'A new Flutter project.' + s.description = <<-DESC +A new Flutter project. + DESC + s.homepage = 'http://example.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Your Company' => 'email@example.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/lib/frame_dependency_manager.dart b/lib/frame_dependency_manager.dart new file mode 100644 index 0000000..b564914 --- /dev/null +++ b/lib/frame_dependency_manager.dart @@ -0,0 +1,35 @@ +import 'dart:typed_data'; + +/// SPS/PPS/I帧依赖关系管理器 +class FrameDependencyManager { + Uint8List? _sps; + Uint8List? _pps; + final int windowSize = 30; + final List _iFrameSeqWindow = []; + + /// 更新SPS缓存 + void updateSps(Uint8List? sps) { + _sps = sps; + } + /// 更新PPS缓存 + void updatePps(Uint8List? pps) { + _pps = pps; + } + Uint8List? get sps => _sps; + Uint8List? get pps => _pps; + + /// 判断是否有可用I帧 + bool get hasIFrame => _iFrameSeqWindow.isNotEmpty; + int? get lastIFrameSeq => _iFrameSeqWindow.isNotEmpty ? _iFrameSeqWindow.last : null; + void updateIFrameSeq(int seq) { + _iFrameSeqWindow.add(seq); + if (_iFrameSeqWindow.length > windowSize) { + _iFrameSeqWindow.removeAt(0); + } + } + + /// 判断指定I帧序号是否在滑动窗口内 + bool isIFrameDecoded(int? seq) { + return seq != null && _iFrameSeqWindow.contains(seq); + } +} \ No newline at end of file diff --git a/lib/nalu_utils.dart b/lib/nalu_utils.dart new file mode 100644 index 0000000..1615081 --- /dev/null +++ b/lib/nalu_utils.dart @@ -0,0 +1,70 @@ +/// NALU相关工具类与结构体 +import 'dart:typed_data'; + +/// NALU单元结构体 +class NaluUnit { + final int type; // NALU类型 + final List data; + NaluUnit(this.type, this.data); +} + +class NaluUtils { + /// 分离一帧数据中的所有NALU单元 + static List splitNalus(List data) { + final List nalus = []; + int i = 0; + List startCodes = []; + // 先找到所有起始码位置 + while (i < data.length - 3) { + if (i < data.length - 4 && + data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 0 && data[i + 3] == 1) { + startCodes.add(i); + i += 4; + } else if (data[i] == 0 && data[i + 1] == 0 && data[i + 2] == 1) { + startCodes.add(i); + i += 3; + } else { + i++; + } + } + // 补上结尾 + startCodes.add(data.length); + // 分割NALU + int nalusTotalLen = 0; + for (int idx = 0; idx < startCodes.length - 1; idx++) { + int start = startCodes[idx]; + int next = startCodes[idx + 1]; + int skip = (data[start] == 0 && data[start + 1] == 0 && data[start + 2] == 0 && data[start + 3] == 1) ? 4 : 3; + int naluStart = start + skip; + if (naluStart < next) { + final nalu = data.sublist(start, next); + nalusTotalLen += nalu.length; + if (nalu.isNotEmpty) { + nalus.add(NaluUnit(getNaluType(nalu), nalu)); + } + } + } + if (nalus.isEmpty && data.isNotEmpty) { + nalus.add(NaluUnit(getNaluType(data), data)); + } else if (nalusTotalLen < data.length) { + nalus.add(NaluUnit(getNaluType(data.sublist(nalusTotalLen)), data.sublist(nalusTotalLen))); + } + return nalus; + } + + /// 获取NALU类型 + static int getNaluType(List nalu) { + if (nalu.isEmpty) return -1; + int offset = 0; + if (nalu.length >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00) { + if (nalu[2] == 0x01) + offset = 3; + else if (nalu[2] == 0x00 && nalu[3] == 0x01) + offset = 4; + } + if (nalu.length > offset) { + return nalu[offset] & 0x1F; + } + return -1; + } +} \ No newline at end of file diff --git a/lib/video_decode_plugin.dart b/lib/video_decode_plugin.dart new file mode 100644 index 0000000..2356ed9 --- /dev/null +++ b/lib/video_decode_plugin.dart @@ -0,0 +1,346 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'video_decode_plugin_platform_interface.dart'; +import 'nalu_utils.dart'; +import 'frame_dependency_manager.dart'; + +/// 视频解码器配置 +class VideoDecoderConfig { + /// 视频宽度 + final int width; + + /// 视频高度 + final int height; + + /// 编码类型,默认h264 + final String codecType; + + /// 构造函数 + VideoDecoderConfig({ + required this.width, + required this.height, + this.codecType = 'h264', + }); + + /// 转换为Map + Map toMap() { + return { + 'width': width, + 'height': height, + 'codecType': codecType, + }; + } +} + +/// 视频解码插件主类 +class VideoDecodePlugin { + static const MethodChannel _channel = MethodChannel('video_decode_plugin'); + + static int? _textureId; + + /// 获取默认纹理ID + static int? get textureId => _textureId; + + static final _depManager = FrameDependencyManager(); + + /// onFrameRendered回调类型(解码并已开始渲染) + static void Function(int textureId)? _onFrameRendered; + + /// 设置onFrameRendered监听 + static void setOnFrameRenderedListener( + void Function(int textureId) callback) { + _onFrameRendered = callback; + _channel.setMethodCallHandler(_handleMethodCall); + } + + static Future _handleMethodCall(MethodCall call) async { + if (call.method == 'onFrameRendered') { + final int? textureId = call.arguments['textureId']; + if (_onFrameRendered != null && textureId != null) { + _onFrameRendered!(textureId); + } + } + } + + /// 初始化解码器 + static Future initDecoder(VideoDecoderConfig config) async { + final textureId = + await _channel.invokeMethod('initDecoder', config.toMap()); + _textureId = textureId; + return textureId; + } + + /// 解码视频帧(参数扩展,仅供内部调用) + static Future _decodeFrame({ + required Uint8List frameData, + required int frameType, // 0=I帧, 1=P帧 + required int timestamp, // 毫秒或微秒 + required int frameSeq, // 帧序号 + int? refIFrameSeq, // P帧时可选 + }) async { + if (_textureId == null) return false; + final params = { + 'textureId': _textureId, + 'frameData': frameData, + 'frameType': frameType, + 'timestamp': timestamp, + 'frameSeq': frameSeq, + if (refIFrameSeq != null) 'refIFrameSeq': refIFrameSeq, + }; + final result = await _channel.invokeMethod('decodeFrame', params); + return result ?? false; + } + + /// 释放解码器资源 + static Future releaseDecoder() async { + if (_textureId == null) return true; + final result = await _channel.invokeMethod('releaseDecoder', { + 'textureId': _textureId, + }); + _textureId = null; + _depManager.updateSps(null); + _depManager.updatePps(null); + return result ?? false; + } + + /// 获取平台版本 + static Future getPlatformVersion() { + return VideoDecodePluginPlatform.instance.getPlatformVersion(); + } + + /// 检查当前平台是否支持 + static bool get isPlatformSupported { + return Platform.isAndroid || Platform.isIOS; + } + + /// + /// [frameData]:帧数据 + /// [frameType]:帧类型 0=I帧, 1=P帧 + /// [timestamp]:帧时间戳(绝对时间戳) + /// [frameSeq]:帧序号 + /// [splitNalFromIFrame]:true时遇到I帧自动从I帧分割NALU并依赖管理,false时直接发送原始数据(适配SPS/PPS/I帧独立推送场景)。 + /// + static Future sendFrame({ + required List frameData, + required int frameType, + required int timestamp, + required int frameSeq, + bool splitNalFromIFrame = false, + int? refIFrameSeq, // P帧时可选 + }) async { + if (Platform.isAndroid) { + await _sendFrameAndroid( + frameData: frameData, + frameType: frameType, + timestamp: timestamp, + frameSeq: frameSeq, + splitNalFromIFrame: splitNalFromIFrame, + refIFrameSeq: refIFrameSeq, + ); + } else if (Platform.isIOS) { + await _sendFrameIOS( + frameData: frameData, + frameType: frameType, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: refIFrameSeq, + ); + } + // 其他平台暂不支持 + } + + static Future _sendFrameAndroid({ + required List frameData, + required int frameType, + required int timestamp, + required int frameSeq, + bool splitNalFromIFrame = false, + int? refIFrameSeq, + }) async { + if (splitNalFromIFrame && frameType == 0) { + // 优先使用缓存的SPS/PPS + if (_depManager.sps != null && _depManager.pps != null) { + await _decodeFrame( + frameData: _depManager.sps!, + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq - 2, + refIFrameSeq: frameSeq - 2, + ); + await _decodeFrame( + frameData: _depManager.pps!, + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq - 1, + refIFrameSeq: frameSeq - 1, + ); + await _decodeFrame( + frameData: Uint8List.fromList(frameData), + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: frameSeq, + ); + _depManager.updateIFrameSeq(frameSeq); + return; + } + // 首次无缓存时分割并缓存SPS/PPS + final nalus = NaluUtils.splitNalus(frameData); + + List? sps, pps; + for (final nalu in nalus) { + if (nalu.type == 7) + sps = nalu.data; + else if (nalu.type == 8) pps = nalu.data; + } + if (sps != null) { + _depManager.updateSps(Uint8List.fromList(sps)); + } + if (pps != null) { + _depManager.updatePps(Uint8List.fromList(pps)); + } + if (_depManager.sps == null || _depManager.pps == null) { + print('[VideoDecodePlugin] 丢弃I帧: 未缓存SPS/PPS'); + return; + } + await _decodeFrame( + frameData: _depManager.sps!, + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq - 2, + refIFrameSeq: frameSeq - 2, + ); + await _decodeFrame( + frameData: _depManager.pps!, + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq - 1, + refIFrameSeq: frameSeq - 1, + ); + await _decodeFrame( + frameData: Uint8List.fromList(frameData), + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: frameSeq, + ); + _depManager.updateIFrameSeq(frameSeq); + return; + } + // 兼容直接推送SPS/PPS/I帧/P帧等场景,直接发送 + // P帧依赖链完整性校验(提前) + if (frameType == 1) { + if (!_depManager.isIFrameDecoded(refIFrameSeq)) { + print( + '[丢帧] P帧依赖的I帧未解码,丢弃 frameSeq=$frameSeq, refIFrameSeq=$refIFrameSeq'); + return; + } + } + await _decodeFrame( + frameData: Uint8List.fromList(frameData), + frameType: frameType, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: frameType == 0 ? frameSeq : _depManager.lastIFrameSeq, + ); + // 若为I帧,更新依赖管理 + if (frameType == 0) _depManager.updateIFrameSeq(frameSeq); + } + + static Future _sendFrameIOS({ + required List frameData, + required int frameType, + required int timestamp, + required int frameSeq, + int? refIFrameSeq, + }) async { + // 仅P帧做AnnexB起始码检测和修正 + if (frameType == 1) { + // 分割NALU并只保留type=1的NALU + final nalus = NaluUtils.splitNalus(frameData); + final pNalus = nalus.where((nalu) => nalu.type == 1).toList(); + if (pNalus.isEmpty) { + print('[VideoDecodePlugin][警告] iOS端P帧未找到type=1的NALU,已丢弃'); + return; + } + // 拼接所有type=1的NALU + final sendData = []; + for (final nalu in pNalus) { + sendData.addAll(nalu.data); + } + // print( + // '[VideoDecodePlugin][调试] iOS端P帧仅推送type=1 NALU, count=${pNalus.length}, 总长度=${sendData.length}'); + frameData = sendData; + int startIndex = -1; + for (int i = 0; i < frameData.length - 3; i++) { + if (frameData[i] == 0x00 && + frameData[i + 1] == 0x00 && + frameData[i + 2] == 0x00 && + frameData[i + 3] == 0x01) { + startIndex = i; + break; + } + } + if (startIndex == -1) { + print('[VideoDecodePlugin][警告] iOS端P帧仅type=1 NALU后无AnnexB起始码,已丢弃'); + return; + } + if (startIndex > 0) { + frameData = frameData.sublist(startIndex); + } + } + if (frameType == 0) { + // I帧需分割NALU,严格按SPS->PPS->I帧顺序推送 + final nalus = NaluUtils.splitNalus(frameData); + List? sps, pps, idr; + for (final nalu in nalus) { + if (nalu.type == 7) sps = nalu.data; + if (nalu.type == 8) pps = nalu.data; + if (nalu.type == 5) idr = nalu.data; + } + if (sps != null) { + await _decodeFrame( + frameData: Uint8List.fromList(sps), + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: frameSeq, + ); + } + if (pps != null) { + await _decodeFrame( + frameData: Uint8List.fromList(pps), + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: frameSeq, + ); + } + if (idr != null) { + await _decodeFrame( + frameData: Uint8List.fromList(idr), + frameType: 0, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: frameSeq, + ); + } + _depManager.updateIFrameSeq(frameSeq); + return; + } + // 其他类型帧直接推送 + await _decodeFrame( + frameData: Uint8List.fromList(frameData), + frameType: frameType, + timestamp: timestamp, + frameSeq: frameSeq, + refIFrameSeq: refIFrameSeq, + ); + if (frameType == 0) _depManager.updateIFrameSeq(frameSeq); + } +} diff --git a/lib/video_decode_plugin_method_channel.dart b/lib/video_decode_plugin_method_channel.dart new file mode 100644 index 0000000..b81c5fc --- /dev/null +++ b/lib/video_decode_plugin_method_channel.dart @@ -0,0 +1,18 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'video_decode_plugin_platform_interface.dart'; + +/// 方法通道实现 +class MethodChannelVideoDecodePlugin extends VideoDecodePluginPlatform { + /// 方法通道 + @visibleForTesting + final methodChannel = const MethodChannel('video_decode_plugin'); + + @override + Future getPlatformVersion() async { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } +} diff --git a/lib/video_decode_plugin_platform_interface.dart b/lib/video_decode_plugin_platform_interface.dart new file mode 100644 index 0000000..46455b8 --- /dev/null +++ b/lib/video_decode_plugin_platform_interface.dart @@ -0,0 +1,30 @@ +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'video_decode_plugin_method_channel.dart'; + +abstract class VideoDecodePluginPlatform extends PlatformInterface { + /// Constructs a VideoDecodePluginPlatform. + VideoDecodePluginPlatform() : super(token: _token); + + static final Object _token = Object(); + + static VideoDecodePluginPlatform _instance = MethodChannelVideoDecodePlugin(); + + /// The default instance of [VideoDecodePluginPlatform] to use. + /// + /// Defaults to [MethodChannelVideoDecodePlugin]. + static VideoDecodePluginPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [VideoDecodePluginPlatform] when + /// they register themselves. + static set instance(VideoDecodePluginPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// 获取平台版本 + Future getPlatformVersion() { + throw UnimplementedError('platformVersion() has not been implemented.'); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..dd1cd5a --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,72 @@ +name: video_decode_plugin +description: "A new Flutter project." +version: 0.0.1 +homepage: + +environment: + sdk: '>=3.3.4 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: top.skychip.video_decode_plugin + pluginClass: VideoDecodePlugin + ios: + pluginClass: VideoDecodePlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages