feat:init
29
.gitignore
vendored
Normal file
@ -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/
|
||||
33
.metadata
Normal file
@ -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'
|
||||
3
CHANGELOG.md
Normal file
@ -0,0 +1,3 @@
|
||||
## 0.0.1
|
||||
|
||||
* TODO: Describe initial release.
|
||||
176
README.md
@ -1,93 +1,151 @@
|
||||
# video_decode_plugin
|
||||
|
||||
一个高性能的 Flutter 插件,用于在 Android 原生层解码 H.264 裸流数据,并支持两种渲染模式。
|
||||
|
||||
## 功能特点
|
||||
|
||||
## Getting started
|
||||
- 支持 H.264 Annex B 格式裸流解码(含 NALU 单元)
|
||||
- 使用 Android MediaCodec 硬解码,提供高性能解码能力
|
||||
- 支持两种渲染模式:
|
||||
- Flutter 纹理渲染:将解码后的帧通过 Flutter Texture 传递到 Flutter UI
|
||||
- 原生 SurfaceView 渲染:在原生 Android 层直接渲染
|
||||
- 提供完善的配置管理和性能监控
|
||||
- 支持动态丢帧策略,优化内存使用
|
||||
- 适配低端设备的性能优化措施
|
||||
|
||||
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)!
|
||||
在 `pubspec.yaml` 文件中添加依赖:
|
||||
|
||||
## 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)
|
||||
### 基本用法
|
||||
|
||||
## Collaborate with your team
|
||||
```dart
|
||||
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
- [ ] [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)
|
||||
// 创建解码器(Flutter 渲染模式)
|
||||
final decoder = H264Decoder(renderMode: RenderMode.flutter);
|
||||
|
||||
## Test and Deploy
|
||||
// 初始化解码器
|
||||
await decoder.init(
|
||||
const H264DecoderConfig(
|
||||
bufferSize: 10,
|
||||
maxWidth: 1280,
|
||||
maxHeight: 720,
|
||||
useDropFrameStrategy: true,
|
||||
debugMode: true,
|
||||
),
|
||||
);
|
||||
|
||||
Use the built-in continuous integration in GitLab.
|
||||
// 开始解码
|
||||
await decoder.start();
|
||||
|
||||
- [ ] [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)
|
||||
// 输入 H.264 数据
|
||||
await decoder.feedData(h264Data);
|
||||
|
||||
***
|
||||
// 暂停解码
|
||||
await decoder.pause();
|
||||
|
||||
# Editing this README
|
||||
// 恢复解码
|
||||
await decoder.resume();
|
||||
|
||||
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.
|
||||
// 释放资源
|
||||
await decoder.release();
|
||||
```
|
||||
|
||||
## Suggestions for a good README
|
||||
### 渲染视图
|
||||
|
||||
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.
|
||||
#### Flutter 渲染模式
|
||||
|
||||
## Name
|
||||
Choose a self-explaining name for your project.
|
||||
```dart
|
||||
// 使用 Flutter 渲染模式显示视频
|
||||
H264VideoPlayerWidget(
|
||||
decoder: decoder,
|
||||
width: 640,
|
||||
height: 360,
|
||||
backgroundColor: Colors.black,
|
||||
)
|
||||
```
|
||||
|
||||
## 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
|
||||
// 使用原生渲染模式显示视频
|
||||
const H264NativePlayerWidget(
|
||||
width: 640,
|
||||
height: 360,
|
||||
backgroundColor: Colors.black,
|
||||
)
|
||||
```
|
||||
|
||||
## 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.
|
||||
### 监听事件
|
||||
|
||||
## 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.
|
||||
```dart
|
||||
// 订阅解码器事件
|
||||
decoder.eventStream.listen((event) {
|
||||
switch (event.type) {
|
||||
case H264DecoderEventType.frameAvailable:
|
||||
// 新帧可用
|
||||
break;
|
||||
case H264DecoderEventType.stats:
|
||||
// 性能统计信息
|
||||
final stats = event.data as Map<String, dynamic>;
|
||||
print('总帧数: ${stats['totalFrames']}');
|
||||
print('丢弃帧数: ${stats['droppedFrames']}');
|
||||
print('缓冲区使用: ${stats['bufferUsage']}');
|
||||
print('解码耗时: ${stats['lastDecodingTimeMs']}ms');
|
||||
break;
|
||||
case H264DecoderEventType.error:
|
||||
// 解码错误
|
||||
break;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
## 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.
|
||||
## 配置选项
|
||||
|
||||
## 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.
|
||||
`H264DecoderConfig` 类提供以下配置选项:
|
||||
|
||||
## Roadmap
|
||||
If you have ideas for releases in the future, it is a good idea to list them in the README.
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|-----|------|-------|-----|
|
||||
| bufferSize | int | 5 | 缓冲区大小(帧数) |
|
||||
| maxWidth | int | 1280 | 最大解码宽度 |
|
||||
| maxHeight | int | 720 | 最大解码高度 |
|
||||
| useDropFrameStrategy | bool | true | 是否启用丢帧策略 |
|
||||
| debugMode | bool | false | 是否启用调试模式 |
|
||||
|
||||
## 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.
|
||||
1. **动态丢帧策略**:缓冲区满时,优先保留 I 帧,丢弃 P 帧,确保解码连续性。
|
||||
2. **零拷贝传输**:使用 Surface 和 SurfaceTexture 直接渲染,避免内存拷贝。
|
||||
3. **异步处理**:解码和渲染在独立线程进行,不阻塞主线程。
|
||||
4. **低端设备适配**:可设置最大解码分辨率,避免低端设备性能问题。
|
||||
|
||||
## Authors and acknowledgment
|
||||
Show your appreciation to those who have contributed to the project.
|
||||
## 示例应用
|
||||
|
||||
## License
|
||||
For open source projects, say how it is licensed.
|
||||
本项目自带一个完整的示例应用,演示如何使用这个插件播放 H.264 视频流。
|
||||
|
||||
## 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.
|
||||
运行示例:
|
||||
|
||||
```shell
|
||||
cd example
|
||||
flutter run
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 目前仅支持 Android 平台
|
||||
- 需要确保输入的 H.264 数据是有效的 Annex B 格式(含 NALU 开始码)
|
||||
- 建议在实际项目中根据设备性能动态调整解码配置
|
||||
|
||||
## 许可证
|
||||
|
||||
MIT
|
||||
|
||||
4
analysis_options.yaml
Normal file
@ -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
|
||||
9
android/.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
.cxx
|
||||
75
android/build.gradle
Normal file
@ -0,0 +1,75 @@
|
||||
group 'top.skychip.video_decode_plugin'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
ext.kotlin_version = '1.7.10'
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.3.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
}
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
android/settings.gradle
Normal file
@ -0,0 +1 @@
|
||||
rootProject.name = 'video_decode_plugin'
|
||||
13
android/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="top.skychip.video_decode_plugin">
|
||||
|
||||
<!-- 添加硬件加速支持 -->
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||
<uses-feature android:name="android.hardware.camera.autofocus" android:required="false" />
|
||||
<uses-feature android:glEsVersion="0x00020000" android:required="true" />
|
||||
|
||||
<!-- 存储权限,用于示例应用中读取H264文件 -->
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
|
||||
</manifest>
|
||||
@ -0,0 +1,277 @@
|
||||
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
|
||||
|
||||
// 解码器映射表 (纹理ID -> 解码器)
|
||||
private val decoders = ConcurrentHashMap<Long, VideoDecoder>()
|
||||
|
||||
// 已释放的纹理ID集合,用于跟踪防止重用
|
||||
private val releasedTextureIds = HashSet<Long>()
|
||||
|
||||
// 主线程Handler
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
/**
|
||||
* 插件绑定到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) {
|
||||
try {
|
||||
when (call.method) {
|
||||
"getPlatformVersion" -> {
|
||||
handleGetPlatformVersion(result)
|
||||
}
|
||||
"initDecoder" -> {
|
||||
handleInitDecoder(call, result)
|
||||
}
|
||||
"decodeFrame" -> {
|
||||
handleDecodeFrame(call, result)
|
||||
}
|
||||
"releaseDecoder" -> {
|
||||
handleReleaseDecoder(call, result)
|
||||
}
|
||||
"getDecoderStats" -> {
|
||||
handleGetDecoderStats(call, result)
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "处理方法调用失败", e)
|
||||
result.error("NATIVE_ERROR", "处理方法调用失败: ${e.message}", null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台版本
|
||||
*/
|
||||
private fun handleGetPlatformVersion(result: Result) {
|
||||
result.success("Android ${android.os.Build.VERSION.RELEASE}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化解码器
|
||||
*/
|
||||
private fun handleInitDecoder(call: MethodCall, result: Result) {
|
||||
try {
|
||||
// 读取参数
|
||||
val width = call.argument<Int>("width") ?: 640
|
||||
val height = call.argument<Int>("height") ?: 360
|
||||
val frameRate = call.argument<Int?>("frameRate")
|
||||
val codecType = call.argument<String>("codecType") ?: "h264"
|
||||
val bufferSize = call.argument<Int>("bufferSize") ?: 25
|
||||
val threadCount = call.argument<Int>("threadCount") ?: 1
|
||||
val isDebug = call.argument<Boolean>("isDebug") ?: false
|
||||
val enableHardwareDecoder = call.argument<Boolean>("enableHardwareDecoder") ?: true
|
||||
|
||||
// 创建纹理
|
||||
val textureEntry = textureRegistry.createSurfaceTexture()
|
||||
val textureId = textureEntry.id()
|
||||
|
||||
// 检查这个纹理ID是否已经被使用过
|
||||
if (releasedTextureIds.contains(textureId)) {
|
||||
// 如果已经被使用过,说明Flutter引擎在重用纹理ID,这可能导致问题
|
||||
Log.w(TAG, "警告: 纹理ID $textureId 已被使用过,这可能导致问题")
|
||||
|
||||
// 记录这个纹理ID现在是活跃的
|
||||
releasedTextureIds.remove(textureId)
|
||||
}
|
||||
|
||||
// 创建解码器配置
|
||||
val config = VideoDecoderConfig(
|
||||
width = width,
|
||||
height = height,
|
||||
codecType = codecType,
|
||||
frameRate = frameRate,
|
||||
enableHardwareDecoder = enableHardwareDecoder,
|
||||
threadCount = threadCount,
|
||||
bufferSize = bufferSize,
|
||||
isDebug = isDebug
|
||||
)
|
||||
|
||||
// 创建解码器
|
||||
val decoder = VideoDecoder(context, textureEntry, config)
|
||||
|
||||
// 设置回调
|
||||
decoder.callback = object : VideoDecoder.DecoderCallback {
|
||||
override fun onFrameAvailable() {
|
||||
// 通知Flutter刷新纹理
|
||||
runOnMainThread {
|
||||
try {
|
||||
channel.invokeMethod("onFrameAvailable", mapOf("textureId" to textureId))
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "通知Flutter更新纹理失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 保存解码器
|
||||
decoders[textureId] = decoder
|
||||
|
||||
// 返回纹理ID
|
||||
result.success(textureId)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "初始化解码器失败", e)
|
||||
result.error("INIT_FAILED", "初始化解码器失败: ${e.message}", null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码视频帧
|
||||
*/
|
||||
private fun handleDecodeFrame(call: MethodCall, result: Result) {
|
||||
try {
|
||||
// 读取参数
|
||||
val textureId = call.argument<Number>("textureId")?.toLong() ?: return result.error("INVALID_ARGS", "无效的纹理ID", null)
|
||||
val frameData = call.argument<ByteArray>("frameData") ?: return result.error("INVALID_ARGS", "无效的帧数据", null)
|
||||
val frameType = call.argument<Int>("frameType") ?: 0
|
||||
val isIFrame = frameType == 0 // 0表示I帧,1表示P帧
|
||||
|
||||
// 获取解码器
|
||||
val decoder = decoders[textureId] ?: return result.error("DECODER_NOT_FOUND", "找不到纹理ID对应的解码器", null)
|
||||
|
||||
// 解码帧
|
||||
val success = decoder.decodeFrame(frameData, isIFrame)
|
||||
|
||||
// 返回结果
|
||||
result.success(success)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "解码帧失败", e)
|
||||
result.error("DECODE_FAILED", "解码帧失败: ${e.message}", null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放解码器资源
|
||||
*/
|
||||
private fun handleReleaseDecoder(call: MethodCall, result: Result) {
|
||||
try {
|
||||
// 读取参数
|
||||
val textureId = call.argument<Number>("textureId")?.toLong() ?: return result.error("INVALID_ARGS", "无效的纹理ID", null)
|
||||
|
||||
// 获取解码器
|
||||
val decoder = decoders[textureId]
|
||||
if (decoder == null) {
|
||||
// 如果找不到解码器,可能已经释放,直接返回成功
|
||||
result.success(true)
|
||||
return
|
||||
}
|
||||
|
||||
// 释放解码器
|
||||
decoder.release()
|
||||
|
||||
// 从映射中移除
|
||||
decoders.remove(textureId)
|
||||
|
||||
// 记录已释放的纹理ID,以便检测重用
|
||||
releasedTextureIds.add(textureId)
|
||||
|
||||
// 返回成功
|
||||
result.success(true)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "释放解码器失败", e)
|
||||
result.error("RELEASE_FAILED", "释放解码器失败: ${e.message}", null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解码器统计信息
|
||||
*/
|
||||
private fun handleGetDecoderStats(call: MethodCall, result: Result) {
|
||||
try {
|
||||
// 获取纹理ID
|
||||
val textureId = call.argument<Number>("textureId")?.toLong() ?: return result.error("INVALID_ARGS", "无效的纹理ID", null)
|
||||
|
||||
// 获取解码器
|
||||
val decoder = decoders[textureId] ?: return result.error("DECODER_NOT_FOUND", "找不到纹理ID对应的解码器", null)
|
||||
|
||||
// 获取统计信息
|
||||
val stats = decoder.getStatistics()
|
||||
|
||||
// 添加插件级别的信息
|
||||
val enhancedStats = HashMap<String, Any>(stats)
|
||||
enhancedStats["decoderCount"] = decoders.size
|
||||
enhancedStats["textureId"] = textureId
|
||||
|
||||
// 返回统计信息
|
||||
result.success(enhancedStats)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "获取解码器统计信息失败", e)
|
||||
result.error("STATS_FAILED", "获取解码器统计信息失败: ${e.message}", null)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在主线程上执行任务
|
||||
*/
|
||||
private fun runOnMainThread(task: () -> Unit) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
task()
|
||||
} else {
|
||||
mainHandler.post(task)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插件从Flutter引擎解绑时调用
|
||||
*/
|
||||
override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
|
||||
// 释放所有解码器
|
||||
for (decoder in decoders.values) {
|
||||
try {
|
||||
decoder.release()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "插件分离时释放解码器失败", e)
|
||||
}
|
||||
}
|
||||
|
||||
// 清除映射
|
||||
decoders.clear()
|
||||
|
||||
// 移除方法调用处理器
|
||||
channel.setMethodCallHandler(null)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,411 @@
|
||||
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.Build
|
||||
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.atomic.AtomicLong
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
import kotlin.concurrent.withLock
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* 视频解码器
|
||||
* 负责解码H264/H265视频数据并将其渲染到Surface上
|
||||
*/
|
||||
class VideoDecoder(
|
||||
private val context: Context,
|
||||
private val textureEntry: TextureRegistry.SurfaceTextureEntry,
|
||||
private val config: VideoDecoderConfig
|
||||
) {
|
||||
companion object {
|
||||
private const val TAG = "VideoDecoder"
|
||||
private const val TIMEOUT_US = 10000L // 10ms
|
||||
private const val MAX_FRAME_AGE_MS = 100L // 丢弃过旧的帧
|
||||
}
|
||||
|
||||
// 回调接口
|
||||
interface DecoderCallback {
|
||||
fun onFrameAvailable()
|
||||
}
|
||||
|
||||
// 回调实例
|
||||
var callback: DecoderCallback? = null
|
||||
|
||||
// 帧类型枚举
|
||||
enum class FrameType {
|
||||
I_FRAME, P_FRAME, UNKNOWN
|
||||
}
|
||||
|
||||
// 帧结构体
|
||||
private data class Frame(
|
||||
val data: ByteArray,
|
||||
val type: FrameType,
|
||||
val timestamp: Long = System.currentTimeMillis()
|
||||
) {
|
||||
// 检查帧是否过期
|
||||
fun isExpired(): Boolean {
|
||||
return System.currentTimeMillis() - timestamp > MAX_FRAME_AGE_MS
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Frame
|
||||
if (!data.contentEquals(other.data)) return false
|
||||
if (type != other.type) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = data.contentHashCode()
|
||||
result = 31 * result + type.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// SurfaceTexture 和 Surface 用于显示解码后的帧
|
||||
private val surfaceTexture: SurfaceTexture = textureEntry.surfaceTexture()
|
||||
private val surface: Surface = Surface(surfaceTexture)
|
||||
|
||||
// MediaCodec 解码器
|
||||
private var mediaCodec: MediaCodec? = null
|
||||
|
||||
// 待解码的帧队列
|
||||
private val frameQueue = LinkedBlockingQueue<Frame>(config.bufferSize)
|
||||
|
||||
// 解码线程
|
||||
private var decodeThread: Thread? = null
|
||||
private val isRunning = AtomicBoolean(false)
|
||||
|
||||
// 当前解码的帧计数
|
||||
private var frameCount = 0
|
||||
|
||||
// 解码流状态跟踪
|
||||
private val hasReceivedIFrame = AtomicBoolean(false)
|
||||
private val lastIFrameTimestamp = AtomicLong(0)
|
||||
private var droppedFrameCount = 0
|
||||
private var renderedFrameCount = 0
|
||||
|
||||
// 主线程Handler,用于在主线程上更新纹理
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
// 初始化解码器
|
||||
init {
|
||||
try {
|
||||
Log.d(TAG, "初始化解码器: ${config.width}x${config.height}, 编码: ${config.codecType}")
|
||||
|
||||
// 在主线程上设置SurfaceTexture
|
||||
mainHandler.post {
|
||||
try {
|
||||
// 设置SurfaceTexture的默认缓冲区大小
|
||||
surfaceTexture.setDefaultBufferSize(config.width, config.height)
|
||||
Log.d(TAG, "SurfaceTexture缓冲区大小设置为: ${config.width}x${config.height}")
|
||||
|
||||
// 初始化解码器
|
||||
setupDecoder()
|
||||
startDecodeThread()
|
||||
|
||||
// 输出解码器状态
|
||||
mediaCodec?.let {
|
||||
Log.d(TAG, "解码器已启动: ${it.codecInfo.name}")
|
||||
}
|
||||
|
||||
// 延迟100ms通知一个空帧,确保Surface已准备好
|
||||
mainHandler.postDelayed({
|
||||
Log.d(TAG, "发送初始化完成通知")
|
||||
callback?.onFrameAvailable()
|
||||
}, 100)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "初始化解码器失败", e)
|
||||
release()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "创建解码器实例失败", e)
|
||||
release()
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置解码器
|
||||
*/
|
||||
private fun setupDecoder() {
|
||||
try {
|
||||
Log.d(TAG, "开始设置解码器")
|
||||
|
||||
// 确定MIME类型
|
||||
val mimeType = if (config.codecType.lowercase() == "h265") {
|
||||
MediaFormat.MIMETYPE_VIDEO_HEVC
|
||||
} else {
|
||||
MediaFormat.MIMETYPE_VIDEO_AVC // 默认H.264
|
||||
}
|
||||
|
||||
// 创建格式
|
||||
val format = MediaFormat.createVideoFormat(mimeType, config.width, config.height)
|
||||
|
||||
// 配置基本参数
|
||||
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, config.width * config.height)
|
||||
|
||||
// 创建解码器
|
||||
val decoderInstance = if (config.enableHardwareDecoder) {
|
||||
try {
|
||||
MediaCodec.createDecoderByType(mimeType)
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "硬件解码器创建失败,尝试使用软件解码器", e)
|
||||
MediaCodec.createDecoderByType(mimeType)
|
||||
}
|
||||
} else {
|
||||
MediaCodec.createDecoderByType(mimeType)
|
||||
}
|
||||
|
||||
// 配置解码器
|
||||
decoderInstance.configure(format, surface, null, 0)
|
||||
decoderInstance.start()
|
||||
|
||||
// 保存解码器实例
|
||||
mediaCodec = decoderInstance
|
||||
|
||||
// 标记为运行中
|
||||
isRunning.set(true)
|
||||
|
||||
Log.d(TAG, "解码器设置完成: ${decoderInstance.codecInfo.name}")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "设置解码器失败", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动解码线程
|
||||
*/
|
||||
private fun startDecodeThread() {
|
||||
decodeThread = Thread({
|
||||
try {
|
||||
Log.d(TAG, "解码线程已启动")
|
||||
decodeLoop()
|
||||
} catch (e: Exception) {
|
||||
if (isRunning.get()) {
|
||||
Log.e(TAG, "解码线程异常退出", e)
|
||||
}
|
||||
} finally {
|
||||
Log.d(TAG, "解码线程已结束")
|
||||
}
|
||||
}, "VideoDecoderThread")
|
||||
|
||||
decodeThread?.start()
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码主循环
|
||||
*/
|
||||
private fun decodeLoop() {
|
||||
val codec = mediaCodec ?: return
|
||||
Log.d(TAG, "开始解码循环,解码器: ${codec.codecInfo.name}")
|
||||
|
||||
while (isRunning.get()) {
|
||||
try {
|
||||
// 从队列取出一帧
|
||||
val frame = frameQueue.poll(100, TimeUnit.MILLISECONDS)
|
||||
if (frame == null) {
|
||||
continue // 没有帧可解码,继续等待
|
||||
}
|
||||
|
||||
// 处理I帧标志
|
||||
if (frame.type == FrameType.I_FRAME) {
|
||||
hasReceivedIFrame.set(true)
|
||||
lastIFrameTimestamp.set(System.currentTimeMillis())
|
||||
Log.d(TAG, "收到I帧: 大小=${frame.data.size}字节")
|
||||
} else if (!hasReceivedIFrame.get()) {
|
||||
// 如果还没有收到I帧,丢弃P帧
|
||||
droppedFrameCount++
|
||||
continue
|
||||
}
|
||||
|
||||
// 获取输入缓冲区
|
||||
val inputBufferId = codec.dequeueInputBuffer(TIMEOUT_US)
|
||||
if (inputBufferId >= 0) {
|
||||
val inputBuffer = codec.getInputBuffer(inputBufferId)
|
||||
if (inputBuffer != null) {
|
||||
// 将数据复制到缓冲区
|
||||
inputBuffer.clear()
|
||||
inputBuffer.put(frame.data)
|
||||
|
||||
// 提交缓冲区进行解码
|
||||
codec.queueInputBuffer(
|
||||
inputBufferId,
|
||||
0,
|
||||
frame.data.size,
|
||||
System.nanoTime() / 1000,
|
||||
if (frame.type == FrameType.I_FRAME) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理输出缓冲区
|
||||
val bufferInfo = MediaCodec.BufferInfo()
|
||||
var outputBufferId = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
|
||||
|
||||
while (outputBufferId >= 0) {
|
||||
// 将帧渲染到Surface
|
||||
val shouldRender = true // 始终渲染
|
||||
|
||||
if (shouldRender) {
|
||||
codec.releaseOutputBuffer(outputBufferId, true)
|
||||
frameCount++
|
||||
renderedFrameCount++
|
||||
|
||||
// 强制通知
|
||||
if (frameCount % 1 == 0) { // 每帧都通知
|
||||
mainHandler.post {
|
||||
Log.d(TAG, "通知帧可用: 第$frameCount帧")
|
||||
callback?.onFrameAvailable()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
codec.releaseOutputBuffer(outputBufferId, false)
|
||||
droppedFrameCount++
|
||||
}
|
||||
|
||||
// 获取下一个输出缓冲区
|
||||
outputBufferId = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
|
||||
}
|
||||
|
||||
} catch (e: InterruptedException) {
|
||||
Log.d(TAG, "解码线程被中断")
|
||||
break
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "解码循环异常", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解码视频帧
|
||||
*
|
||||
* @param frameData 帧数据
|
||||
* @param isIFrame 是否为I帧
|
||||
* @return 是否成功添加到解码队列
|
||||
*/
|
||||
fun decodeFrame(frameData: ByteArray, isIFrame: Boolean): Boolean {
|
||||
if (!isRunning.get() || frameData.isEmpty()) {
|
||||
Log.w(TAG, "解码器未运行或帧数据为空")
|
||||
return false
|
||||
}
|
||||
|
||||
try {
|
||||
// 创建帧对象
|
||||
val frameType = if (isIFrame) FrameType.I_FRAME else FrameType.P_FRAME
|
||||
val frame = Frame(frameData, frameType)
|
||||
|
||||
// 对于I帧记录日志
|
||||
if (isIFrame) {
|
||||
Log.d(TAG, "添加I帧到队列: 大小=${frameData.size}字节")
|
||||
}
|
||||
|
||||
// 将帧添加到队列
|
||||
return if (frameQueue.offer(frame)) {
|
||||
true
|
||||
} else {
|
||||
// 队列已满,移除一帧后再添加
|
||||
frameQueue.poll()
|
||||
droppedFrameCount++
|
||||
frameQueue.offer(frame)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "添加帧到解码队列失败", e)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取解码统计信息
|
||||
*/
|
||||
fun getStatistics(): Map<String, Any> {
|
||||
return mapOf(
|
||||
"totalFrames" to frameCount,
|
||||
"renderedFrames" to renderedFrameCount,
|
||||
"droppedFrames" to droppedFrameCount,
|
||||
"queueSize" to frameQueue.size,
|
||||
"hasIFrame" to hasReceivedIFrame.get(),
|
||||
"lastIFrameAgeMs" to (System.currentTimeMillis() - lastIFrameTimestamp.get())
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 释放资源
|
||||
*/
|
||||
fun release() {
|
||||
Log.d(TAG, "开始释放解码器资源")
|
||||
|
||||
// 标记为停止运行
|
||||
isRunning.set(false)
|
||||
|
||||
// 清除回调
|
||||
callback = null
|
||||
|
||||
try {
|
||||
// 停止解码线程
|
||||
decodeThread?.let { thread ->
|
||||
thread.interrupt()
|
||||
try {
|
||||
thread.join(500) // 等待最多500ms
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "等待解码线程结束超时", e)
|
||||
}
|
||||
}
|
||||
decodeThread = null
|
||||
|
||||
// 释放MediaCodec
|
||||
mediaCodec?.let { codec ->
|
||||
try {
|
||||
codec.stop()
|
||||
codec.release()
|
||||
Log.d(TAG, "MediaCodec已释放")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "释放MediaCodec失败", e)
|
||||
}
|
||||
}
|
||||
mediaCodec = null
|
||||
|
||||
// 清空队列
|
||||
frameQueue.clear()
|
||||
|
||||
// 释放Surface
|
||||
try {
|
||||
surface.release()
|
||||
Log.d(TAG, "Surface已释放")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "释放Surface失败", e)
|
||||
}
|
||||
|
||||
// 释放纹理
|
||||
try {
|
||||
textureEntry.release()
|
||||
Log.d(TAG, "TextureEntry已释放")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "释放TextureEntry失败", e)
|
||||
}
|
||||
|
||||
Log.d(TAG, "所有资源释放完成")
|
||||
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "释放资源失败", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package top.skychip.video_decode_plugin
|
||||
|
||||
/**
|
||||
* 视频解码器配置
|
||||
*
|
||||
* @param width 视频宽度
|
||||
* @param height 视频高度
|
||||
* @param codecType 编解码器类型,默认为h264
|
||||
* @param frameRate 帧率,可为空
|
||||
* @param enableHardwareDecoder 是否启用硬件解码
|
||||
* @param threadCount 解码线程数
|
||||
* @param bufferSize 输入缓冲区大小
|
||||
* @param isDebug 是否开启调试日志
|
||||
*/
|
||||
data class VideoDecoderConfig(
|
||||
val width: Int,
|
||||
val height: Int,
|
||||
val codecType: String = "h264",
|
||||
val frameRate: Int? = null,
|
||||
val enableHardwareDecoder: Boolean = true,
|
||||
val threadCount: Int = 1,
|
||||
val bufferSize: Int = 10,
|
||||
val isDebug: Boolean = false
|
||||
)
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
43
example/.gitignore
vendored
Normal file
@ -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
|
||||
16
example/README.md
Normal file
@ -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.
|
||||
28
example/analysis_options.yaml
Normal file
@ -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
|
||||
13
example/android/.gitignore
vendored
Normal file
@ -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
|
||||
67
example/android/app/build.gradle
Normal file
@ -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 {}
|
||||
7
example/android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
44
example/android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,44 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<application
|
||||
android:label="video_decode_plugin_example"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:launchMode="singleTop"
|
||||
android:theme="@style/LaunchTheme"
|
||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||
android:hardwareAccelerated="true"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||
the Android process has started. This theme is visible to the user
|
||||
while the Flutter UI initializes. After that, this theme continues
|
||||
to determine the Window background behind the Flutter UI. -->
|
||||
<meta-data
|
||||
android:name="io.flutter.embedding.android.NormalTheme"
|
||||
android:resource="@style/NormalTheme"
|
||||
/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
android:name="flutterEmbedding"
|
||||
android:value="2" />
|
||||
</application>
|
||||
<!-- Required to query activities that can process text, see:
|
||||
https://developer.android.com/training/package-visibility?hl=en and
|
||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||
|
||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
@ -0,0 +1,5 @@
|
||||
package top.skychip.video_decode_plugin_example
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
class MainActivity: FlutterActivity()
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="?android:colorBackground" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Modify this file to customize your launch splash screen -->
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@android:color/white" />
|
||||
|
||||
<!-- You can insert your own image assets here -->
|
||||
<!-- <item>
|
||||
<bitmap
|
||||
android:gravity="center"
|
||||
android:src="@mipmap/launch_image" />
|
||||
</item> -->
|
||||
</layer-list>
|
||||
BIN
example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 544 B |
BIN
example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 442 B |
BIN
example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 721 B |
BIN
example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.0 KiB |
BIN
example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
18
example/android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
18
example/android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<!-- Show a splash screen on the activity. Automatically removed when
|
||||
the Flutter engine draws its first frame -->
|
||||
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||
</style>
|
||||
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||
This theme determines the color of the Android Window while your
|
||||
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||
running.
|
||||
|
||||
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||
<item name="android:windowBackground">?android:colorBackground</item>
|
||||
</style>
|
||||
</resources>
|
||||
7
example/android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<!-- The INTERNET permission is required for development. Specifically,
|
||||
the Flutter tool needs it to communicate with the running application
|
||||
to allow setting breakpoints, to provide hot reload, etc.
|
||||
-->
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
</manifest>
|
||||
18
example/android/build.gradle
Normal file
@ -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
|
||||
}
|
||||
4
example/android/gradle.properties
Normal file
@ -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
|
||||
5
example/android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip
|
||||
26
example/android/settings.gradle
Normal file
@ -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"
|
||||
BIN
example/assets/demo.h264
Normal file
34
example/ios/.gitignore
vendored
Normal file
@ -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
|
||||
26
example/ios/Flutter/AppFrameworkInfo.plist
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>App</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>io.flutter.flutter.app</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>App</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1.0</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>12.0</string>
|
||||
</dict>
|
||||
</plist>
|
||||
2
example/ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
2
example/ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
44
example/ios/Podfile
Normal file
@ -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
|
||||
619
example/ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,619 @@
|
||||
// !$*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 */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
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 = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||
);
|
||||
path = RunnerTests;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||
);
|
||||
name = Flutter;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146E51CF9000F007C117D = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
97C146EF1CF9000F007C117D /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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 = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||
buildPhases = (
|
||||
331C807D294A63A400263BE5 /* Sources */,
|
||||
331C807F294A63A400263BE5 /* Resources */,
|
||||
);
|
||||
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 = (
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
);
|
||||
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";
|
||||
};
|
||||
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";
|
||||
};
|
||||
/* 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 = "<group>";
|
||||
};
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
97C147001CF9000F007C117D /* Base */,
|
||||
);
|
||||
name = LaunchScreen.storyboard;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* 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;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = U3J8U8WN26;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
VERSIONING_SYSTEM = "apple-generic";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
331C8088294A63A400263BE5 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
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;
|
||||
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;
|
||||
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;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = U3J8U8WN26;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
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;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = U3J8U8WN26;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = top.skychip.videoDecodePluginExample;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
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 */;
|
||||
}
|
||||
7
example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,98 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1510"
|
||||
version = "1.3">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
buildForRunning = "YES"
|
||||
buildForProfiling = "YES"
|
||||
buildForArchiving = "YES"
|
||||
buildForAnalyzing = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildActionEntry>
|
||||
</BuildActionEntries>
|
||||
</BuildAction>
|
||||
<TestAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<Testables>
|
||||
<TestableReference
|
||||
skipped = "NO"
|
||||
parallelizable = "YES">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||
BuildableName = "RunnerTests.xctest"
|
||||
BlueprintName = "RunnerTests"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</TestableReference>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
launchStyle = "0"
|
||||
useCustomWorkingDirectory = "NO"
|
||||
ignoresPersistentStateOnLaunch = "NO"
|
||||
debugDocumentVersioning = "YES"
|
||||
debugServiceExtension = "internal"
|
||||
allowLocationSimulation = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||
savedToolIdentifier = ""
|
||||
useCustomWorkingDirectory = "NO"
|
||||
debugDocumentVersioning = "YES">
|
||||
<BuildableProductRunnable
|
||||
runnableDebuggingMode = "0">
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
</ProfileAction>
|
||||
<AnalyzeAction
|
||||
buildConfiguration = "Debug">
|
||||
</AnalyzeAction>
|
||||
<ArchiveAction
|
||||
buildConfiguration = "Release"
|
||||
revealArchiveInOrganizer = "YES">
|
||||
</ArchiveAction>
|
||||
</Scheme>
|
||||
7
example/ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>PreviewsEnabled</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
13
example/ios/Runner/AppDelegate.swift
Normal file
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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"
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 295 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 450 B |
|
After Width: | Height: | Size: 282 B |
|
After Width: | Height: | Size: 462 B |
|
After Width: | Height: | Size: 704 B |
|
After Width: | Height: | Size: 406 B |
|
After Width: | Height: | Size: 586 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 862 B |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 762 B |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.4 KiB |
23
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -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"
|
||||
}
|
||||
}
|
||||
BIN
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -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.
|
||||
37
example/ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--View Controller-->
|
||||
<scene sceneID="EHf-IW-A2E">
|
||||
<objects>
|
||||
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||
</imageView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
<constraints>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||
</constraints>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="53" y="375"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
<image name="LaunchImage" width="168" height="185"/>
|
||||
</resources>
|
||||
</document>
|
||||
26
example/ios/Runner/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--Flutter View Controller-->
|
||||
<scene sceneID="tne-QT-ifu">
|
||||
<objects>
|
||||
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||
</view>
|
||||
</viewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
</scene>
|
||||
</scenes>
|
||||
</document>
|
||||
49
example/ios/Runner/Info.plist
Normal file
@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Video Decode Plugin</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>video_decode_plugin_example</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
1
example/ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
26
example/ios/RunnerTests/RunnerTests.swift
Normal file
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
137
example/lib/h264_frame_generator.dart
Normal file
@ -0,0 +1,137 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// H.264帧生成器
|
||||
/// 生成简单的H.264帧数据,用于测试
|
||||
class H264FrameGenerator {
|
||||
// 视频宽度
|
||||
final int width;
|
||||
|
||||
// 视频高度
|
||||
final int height;
|
||||
|
||||
// 序列参数集 (SPS) - 一个简单的示例
|
||||
// 注意:这不是一个完全有效的SPS,仅用于测试
|
||||
final List<int> _spsData = [
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x67,
|
||||
0x42,
|
||||
0x00,
|
||||
0x0A,
|
||||
0xF8,
|
||||
0x41,
|
||||
0xA2,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x01,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x32,
|
||||
0x0F,
|
||||
0x18,
|
||||
0x31,
|
||||
0x8C
|
||||
];
|
||||
|
||||
// 图像参数集 (PPS) - 一个简单的示例
|
||||
// 注意:这不是一个完全有效的PPS,仅用于测试
|
||||
final List<int> _ppsData = [0x00, 0x00, 0x00, 0x01, 0x68, 0xCE, 0x38, 0x80];
|
||||
|
||||
// I帧的起始数据示例 - 这只是一个示例头部
|
||||
final List<int> _iFrameHeader = [
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x65,
|
||||
0x88,
|
||||
0x80,
|
||||
0x00,
|
||||
0x00,
|
||||
0x03,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00
|
||||
];
|
||||
|
||||
// P帧的起始数据示例 - 这只是一个示例头部
|
||||
final List<int> _pFrameHeader = [
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
0x01,
|
||||
0x41,
|
||||
0x9A,
|
||||
0x1C,
|
||||
0x0D,
|
||||
0x3E,
|
||||
0x04
|
||||
];
|
||||
|
||||
// 当前帧计数
|
||||
int _frameCount = 0;
|
||||
|
||||
// 每个I帧之间的P帧数量
|
||||
final int _pFramesPerIFrame = 9;
|
||||
|
||||
/// 构造函数
|
||||
H264FrameGenerator({
|
||||
required this.width,
|
||||
required this.height,
|
||||
});
|
||||
|
||||
/// 获取SPS和PPS数据
|
||||
Uint8List getConfigurationData() {
|
||||
// 组合SPS和PPS
|
||||
List<int> data = [];
|
||||
data.addAll(_spsData);
|
||||
data.addAll(_ppsData);
|
||||
return Uint8List.fromList(data);
|
||||
}
|
||||
|
||||
/// 生成下一帧数据
|
||||
/// 返回 (帧数据, 是否为I帧)
|
||||
(Uint8List, bool) generateNextFrame() {
|
||||
_frameCount++;
|
||||
|
||||
// 每10帧插入一个I帧
|
||||
bool isIFrame = _frameCount % (_pFramesPerIFrame + 1) == 1;
|
||||
|
||||
List<int> frameData = [];
|
||||
|
||||
if (isIFrame) {
|
||||
// I帧: 添加SPS和PPS,并添加I帧数据
|
||||
frameData.addAll(_spsData);
|
||||
frameData.addAll(_ppsData);
|
||||
frameData.addAll(_iFrameHeader);
|
||||
|
||||
// 添加一些模拟的I帧数据
|
||||
// 实际中,这里应该是真实的编码数据
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
frameData.add((i * 13) % 256);
|
||||
}
|
||||
} else {
|
||||
// P帧: 只添加P帧数据
|
||||
frameData.addAll(_pFrameHeader);
|
||||
|
||||
// 添加一些模拟的P帧数据
|
||||
// 实际中,这里应该是真实的编码数据
|
||||
for (int i = 0; i < 300; i++) {
|
||||
frameData.add((i * 7 + _frameCount) % 256);
|
||||
}
|
||||
}
|
||||
|
||||
return (Uint8List.fromList(frameData), isIFrame);
|
||||
}
|
||||
|
||||
/// 重置帧计数
|
||||
void reset() {
|
||||
_frameCount = 0;
|
||||
}
|
||||
}
|
||||
834
example/lib/main.dart
Normal file
@ -0,0 +1,834 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:video_decode_plugin/video_decode_plugin.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
}
|
||||
|
||||
class MyApp extends StatefulWidget {
|
||||
const MyApp({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<MyApp> createState() => _MyAppState();
|
||||
}
|
||||
|
||||
class _MyAppState extends State<MyApp> {
|
||||
int? _textureId;
|
||||
bool _isInitialized = false;
|
||||
bool _isPlaying = false;
|
||||
String _statusMessage = '等待初始化...';
|
||||
|
||||
// 视频分辨率和帧率
|
||||
final int _width = 640;
|
||||
final int _height = 360;
|
||||
final int _frameRate = 25;
|
||||
|
||||
// 解码定时器
|
||||
Timer? _decodeTimer;
|
||||
|
||||
// 状态监控定时器
|
||||
Timer? _statsTimer;
|
||||
|
||||
// 添加一个计时器记录帧率
|
||||
Stopwatch? _frameRateWatch;
|
||||
|
||||
// H.264文件数据
|
||||
Uint8List? _h264Data;
|
||||
|
||||
// 已解码的帧数
|
||||
int _frameCount = 0;
|
||||
|
||||
// 接收到的渲染帧数
|
||||
int _renderedFrameCount = 0;
|
||||
|
||||
// 状态信息
|
||||
Map<String, dynamic> _decoderStats = {};
|
||||
|
||||
// 需要强制更新Texture
|
||||
bool _needsTextureUpdate = false;
|
||||
|
||||
// 诊断日志
|
||||
List<String> _logs = [];
|
||||
ScrollController _logScrollController = ScrollController();
|
||||
|
||||
// 帧分隔符 (NAL 单元起始码)
|
||||
final List<int> _startCode = [0, 0, 0, 1];
|
||||
|
||||
// 当前解析位置
|
||||
int _parsePosition = 0;
|
||||
|
||||
// 帧索引位置列表 (每个帧的起始位置)
|
||||
List<int> _framePositions = [];
|
||||
|
||||
// 帧类型列表
|
||||
List<FrameType> _frameTypes = [];
|
||||
|
||||
// 解码器行为配置
|
||||
final int _bufferSize = 30; // 缓冲区大小(帧数)
|
||||
|
||||
// 是否使用原始数据解析
|
||||
bool _useRawParsing = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadH264File();
|
||||
}
|
||||
|
||||
// 添加日志
|
||||
void _addLog(String message) {
|
||||
print(message); // 同时打印到控制台
|
||||
setState(() {
|
||||
_logs.add("[${DateTime.now().toString().split('.').first}] $message");
|
||||
|
||||
// 延迟滚动到底部
|
||||
Future.delayed(Duration(milliseconds: 100), () {
|
||||
if (_logScrollController.hasClients) {
|
||||
_logScrollController.animateTo(
|
||||
_logScrollController.position.maxScrollExtent,
|
||||
duration: Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 加载H.264文件
|
||||
Future<void> _loadH264File() async {
|
||||
try {
|
||||
setState(() {
|
||||
_statusMessage = '加载H.264文件中...';
|
||||
_logs = [];
|
||||
});
|
||||
_addLog('开始加载H.264文件');
|
||||
|
||||
// 从assets加载示例H.264文件
|
||||
final ByteData data = await rootBundle.load('assets/demo.h264');
|
||||
_h264Data = data.buffer.asUint8List();
|
||||
|
||||
_addLog(
|
||||
'H.264文件加载完成,大小: ${(_h264Data!.length / 1024).toStringAsFixed(2)} KB');
|
||||
|
||||
setState(() {
|
||||
_statusMessage =
|
||||
'H.264文件加载完成,大小: ${(_h264Data!.length / 1024).toStringAsFixed(2)} KB';
|
||||
});
|
||||
|
||||
// 预解析H.264文件
|
||||
_parseH264File();
|
||||
} catch (e) {
|
||||
_addLog('H.264文件加载失败: $e');
|
||||
setState(() {
|
||||
_statusMessage = 'H.264文件加载失败: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 预解析H.264文件,找出所有帧的位置和类型
|
||||
void _parseH264File() {
|
||||
if (_h264Data == null || _h264Data!.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '正在解析H.264文件结构...';
|
||||
});
|
||||
_addLog('开始解析H.264文件结构...');
|
||||
|
||||
_framePositions.clear();
|
||||
_frameTypes.clear();
|
||||
|
||||
// 查找所有起始码位置
|
||||
int iFrameCount = 0;
|
||||
int pFrameCount = 0;
|
||||
|
||||
for (int i = 0; i < _h264Data!.length - 4; i++) {
|
||||
if (_isStartCode(i)) {
|
||||
// 检查NAL类型
|
||||
if (i + 4 < _h264Data!.length) {
|
||||
int nalType = _h264Data![i + 4] & 0x1F;
|
||||
|
||||
_addLog('在位置 $i 找到NAL单元, 类型: $nalType');
|
||||
|
||||
_framePositions.add(i);
|
||||
|
||||
// 根据NAL类型确定帧类型
|
||||
// 5 = IDR帧 (I帧), 7 = SPS, 8 = PPS
|
||||
if (nalType == 5 || nalType == 7 || nalType == 8) {
|
||||
_frameTypes.add(FrameType.iFrame);
|
||||
iFrameCount++;
|
||||
} else {
|
||||
_frameTypes.add(FrameType.pFrame);
|
||||
pFrameCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试直接添加SPS/PPS帧,可能在文件开始处
|
||||
_useRawParsing = (_framePositions.isEmpty || iFrameCount == 0);
|
||||
|
||||
_addLog(
|
||||
'解析完成: 找到 ${_framePositions.length} 个NAL单元, I帧: $iFrameCount, P帧: $pFrameCount');
|
||||
if (_useRawParsing) {
|
||||
_addLog('警告: 未检测到有效I帧,将使用原始数据直接解码');
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage = 'H.264解析完成,共找到 ${_framePositions.length} 个NAL单元';
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否为起始码
|
||||
bool _isStartCode(int position) {
|
||||
if (position + 3 >= _h264Data!.length) return false;
|
||||
return _h264Data![position] == 0 &&
|
||||
_h264Data![position + 1] == 0 &&
|
||||
_h264Data![position + 2] == 0 &&
|
||||
_h264Data![position + 3] == 1;
|
||||
}
|
||||
|
||||
// 帧回调函数
|
||||
void _onFrameAvailable(int textureId) {
|
||||
if (mounted) {
|
||||
_addLog('收到帧可用回调: textureId=$textureId');
|
||||
|
||||
// 必须调用setState刷新UI
|
||||
setState(() {
|
||||
_renderedFrameCount++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 开始统计监控
|
||||
void _startStatsMonitoring() {
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = Timer.periodic(const Duration(milliseconds: 1000), (_) async {
|
||||
if (_textureId != null && mounted) {
|
||||
try {
|
||||
final stats = await VideoDecodePlugin.getDecoderStats(_textureId!);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_decoderStats = stats;
|
||||
});
|
||||
|
||||
// 记录关键的统计变化
|
||||
if (stats['droppedFrames'] > 0 || stats['isBuffering'] == true) {
|
||||
_addLog('解码器状态: ${stats['isBuffering'] ? "缓冲中" : "播放中"}, ' +
|
||||
'输入队列: ${stats['inputQueueSize']}, ' +
|
||||
'输出队列: ${stats['outputQueueSize']}, ' +
|
||||
'丢弃帧: ${stats['droppedFrames']}');
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_addLog('获取统计信息失败: $e');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化解码器
|
||||
Future<void> _initDecoder() async {
|
||||
if (_h264Data == null) {
|
||||
setState(() {
|
||||
_statusMessage = 'H.264文件未加载';
|
||||
});
|
||||
_addLog('错误: H.264文件未加载');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查平台支持
|
||||
if (!VideoDecodePlugin.isPlatformSupported) {
|
||||
setState(() {
|
||||
_statusMessage = '当前平台不支持视频解码';
|
||||
});
|
||||
_addLog('错误: 当前平台不支持视频解码');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '正在初始化解码器...';
|
||||
});
|
||||
_addLog('开始初始化解码器...');
|
||||
|
||||
// 配置解码器
|
||||
final config = VideoDecoderConfig(
|
||||
width: _width,
|
||||
height: _height,
|
||||
frameRate: _frameRate,
|
||||
codecType: CodecType.h264,
|
||||
bufferSize: _bufferSize,
|
||||
threadCount: 2,
|
||||
isDebug: true,
|
||||
enableHardwareDecoder: true,
|
||||
);
|
||||
|
||||
_addLog(
|
||||
'解码器配置: 分辨率 ${_width}x${_height}, 帧率 $_frameRate, 缓冲区 $_bufferSize');
|
||||
|
||||
// 先释放之前的解码器
|
||||
if (_textureId != null) {
|
||||
_addLog('释放旧解码器: $_textureId');
|
||||
await VideoDecodePlugin.releaseDecoder();
|
||||
}
|
||||
|
||||
// 初始化解码器并获取纹理ID
|
||||
final textureId = await VideoDecodePlugin.initDecoder(config);
|
||||
if (textureId == null) {
|
||||
setState(() {
|
||||
_statusMessage = '解码器初始化失败';
|
||||
});
|
||||
_addLog('错误: 解码器初始化失败,返回的textureId为null');
|
||||
return;
|
||||
}
|
||||
|
||||
_addLog('解码器初始化成功,textureId: $textureId');
|
||||
|
||||
// 设置帧可用回调
|
||||
VideoDecodePlugin.setFrameCallbackForTexture(
|
||||
textureId, _onFrameAvailable);
|
||||
_addLog('已设置帧可用回调');
|
||||
|
||||
// 开始监控统计信息
|
||||
_startStatsMonitoring();
|
||||
_addLog('已启动统计信息监控');
|
||||
|
||||
setState(() {
|
||||
_textureId = textureId;
|
||||
_isInitialized = true;
|
||||
_frameCount = 0;
|
||||
_renderedFrameCount = 0;
|
||||
_parsePosition = 0;
|
||||
_needsTextureUpdate = false;
|
||||
_statusMessage = '解码器初始化成功,纹理ID: $_textureId';
|
||||
});
|
||||
|
||||
// 尝试立即解码第一帧I帧
|
||||
await _injectFirstIFrame();
|
||||
} catch (e) {
|
||||
_addLog('初始化解码器错误: $e');
|
||||
setState(() {
|
||||
_statusMessage = '初始化错误: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试立即注入第一个I帧,帮助启动解码
|
||||
Future<void> _injectFirstIFrame() async {
|
||||
if (_h264Data == null || !_isInitialized) return;
|
||||
|
||||
try {
|
||||
_addLog('尝试注入首个I帧进行测试...');
|
||||
|
||||
// 如果找不到有效的I帧位置,直接使用文件开头部分作为I帧
|
||||
if (_useRawParsing || _framePositions.isEmpty) {
|
||||
// 直接使用前1024字节作为I帧
|
||||
int len = _h264Data!.length > 1024 ? 1024 : _h264Data!.length;
|
||||
Uint8List firstFrame = Uint8List(len);
|
||||
firstFrame.setRange(0, len, _h264Data!, 0);
|
||||
|
||||
_addLog('使用原始数据作为I帧进行测试,大小: $len');
|
||||
bool success =
|
||||
await VideoDecodePlugin.decodeFrame(firstFrame, FrameType.iFrame);
|
||||
_addLog('注入测试I帧 ${success ? "成功" : "失败"}');
|
||||
return;
|
||||
}
|
||||
|
||||
// 找到第一个I帧的位置
|
||||
int iFramePos = -1;
|
||||
for (int i = 0; i < _frameTypes.length; i++) {
|
||||
if (_frameTypes[i] == FrameType.iFrame) {
|
||||
iFramePos = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (iFramePos == -1) {
|
||||
_addLog('错误: 未找到I帧');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取I帧数据
|
||||
int startPos = _framePositions[iFramePos];
|
||||
int endPos = (iFramePos + 1 < _framePositions.length)
|
||||
? _framePositions[iFramePos + 1]
|
||||
: _h264Data!.length;
|
||||
|
||||
int frameSize = endPos - startPos;
|
||||
_addLog('找到I帧: 位置 $startPos, 大小 $frameSize');
|
||||
|
||||
// 提取I帧数据
|
||||
Uint8List iFrameData = Uint8List(frameSize);
|
||||
iFrameData.setRange(0, frameSize, _h264Data!, startPos);
|
||||
|
||||
// 解码I帧
|
||||
bool success =
|
||||
await VideoDecodePlugin.decodeFrame(iFrameData, FrameType.iFrame);
|
||||
_addLog('注入I帧 ${success ? "成功" : "失败"}');
|
||||
} catch (e) {
|
||||
_addLog('注入I帧失败: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// 开始播放
|
||||
void _startPlaying() {
|
||||
if (!_isInitialized || _isPlaying || _h264Data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isPlaying = true;
|
||||
_statusMessage = '开始播放...';
|
||||
});
|
||||
_addLog('开始播放, 解码位置: $_parsePosition');
|
||||
|
||||
// 添加强制刷新的逻辑
|
||||
_addDummyFrame();
|
||||
|
||||
// 尝试强制发送第一个I帧
|
||||
_injectFirstIFrame().then((_) {
|
||||
// 重置帧率计时器
|
||||
_frameRateWatch = Stopwatch()..start();
|
||||
|
||||
// 创建定时器以固定帧率解码
|
||||
_decodeTimer =
|
||||
Timer.periodic(Duration(milliseconds: 1000 ~/ _frameRate), (_) {
|
||||
_decodeNextFrame();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 添加一个虚拟帧进行测试
|
||||
void _addDummyFrame() {
|
||||
_addLog('添加测试图形');
|
||||
|
||||
// 创建一个虚拟帧进行测试
|
||||
if (_textureId != null) {
|
||||
// 强制组件立即重绘
|
||||
setState(() {
|
||||
_renderedFrameCount++;
|
||||
});
|
||||
|
||||
// 延迟后再次强制刷新
|
||||
Future.delayed(Duration(milliseconds: 500), () {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_renderedFrameCount++;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 停止播放
|
||||
void _stopPlaying() {
|
||||
_decodeTimer?.cancel();
|
||||
_decodeTimer = null;
|
||||
_frameRateWatch?.stop();
|
||||
|
||||
setState(() {
|
||||
_isPlaying = false;
|
||||
_statusMessage = '播放已停止';
|
||||
});
|
||||
_addLog('播放已停止');
|
||||
}
|
||||
|
||||
// 解码下一帧
|
||||
Future<void> _decodeNextFrame() async {
|
||||
if (!_isInitialized || _h264Data == null) {
|
||||
_stopPlaying();
|
||||
_addLog('解码器未初始化或H264数据为空,停止播放');
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果解析失败,尝试使用原始数据
|
||||
if (_useRawParsing) {
|
||||
await _decodeRawData();
|
||||
return;
|
||||
}
|
||||
|
||||
// 正常解析模式
|
||||
if (_framePositions.isEmpty) {
|
||||
_stopPlaying();
|
||||
_addLog('没有找到有效帧,停止播放');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否播放完毕
|
||||
if (_parsePosition >= _framePositions.length) {
|
||||
// 循环播放,重新开始
|
||||
_parsePosition = 0;
|
||||
setState(() {
|
||||
_statusMessage = '播放完成,重新开始';
|
||||
});
|
||||
_addLog('播放完成,循环回到开始位置');
|
||||
}
|
||||
|
||||
// 获取当前帧位置
|
||||
int currentPos = _framePositions[_parsePosition];
|
||||
|
||||
// 计算帧大小 (到下一帧开始或文件结束)
|
||||
int nextPos = _parsePosition + 1 < _framePositions.length
|
||||
? _framePositions[_parsePosition + 1]
|
||||
: _h264Data!.length;
|
||||
|
||||
int frameSize = nextPos - currentPos;
|
||||
|
||||
// 提取帧数据
|
||||
Uint8List frameData = Uint8List(frameSize);
|
||||
frameData.setRange(0, frameSize, _h264Data!, currentPos);
|
||||
|
||||
// 获取帧类型
|
||||
FrameType frameType = _frameTypes[_parsePosition];
|
||||
|
||||
// 如果是第一帧或每隔一定数量的帧,记录一下详细信息
|
||||
if (_frameCount % 10 == 0 || _frameCount < 5) {
|
||||
String hexPrefix = '';
|
||||
if (frameData.length >= 8) {
|
||||
hexPrefix = '0x' +
|
||||
frameData
|
||||
.sublist(0, 8)
|
||||
.map((e) => e.toRadixString(16).padLeft(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
_addLog(
|
||||
'解码帧 #$_frameCount, 类型: ${frameType == FrameType.iFrame ? "I" : "P"}帧, ' +
|
||||
'大小: ${(frameSize / 1024).toStringAsFixed(2)} KB, 前缀: $hexPrefix');
|
||||
}
|
||||
|
||||
// 解码帧
|
||||
final success = await VideoDecodePlugin.decodeFrame(frameData, frameType);
|
||||
|
||||
// 如果前几帧解码失败,记录详细错误
|
||||
if (!success && _frameCount < 5) {
|
||||
_addLog(
|
||||
'解码失败: 帧 #$_frameCount, 类型: ${frameType == FrameType.iFrame ? "I" : "P"}帧');
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
_frameCount++;
|
||||
_parsePosition++;
|
||||
|
||||
if (mounted) {
|
||||
// 计算实际帧率
|
||||
String frameRateInfo = '';
|
||||
if (_frameRateWatch != null &&
|
||||
_frameRateWatch!.elapsedMilliseconds > 0) {
|
||||
double actualFps =
|
||||
_frameCount / (_frameRateWatch!.elapsedMilliseconds / 1000);
|
||||
frameRateInfo = ', 实际帧率: ${actualFps.toStringAsFixed(1)} fps';
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage =
|
||||
'正在播放: 第${_parsePosition}/${_framePositions.length}帧, ' +
|
||||
'类型: ${frameType == FrameType.iFrame ? "I" : "P"}帧, ' +
|
||||
'大小: ${(frameSize / 1024).toStringAsFixed(2)} KB, ' +
|
||||
'${success ? "成功" : "失败"}$frameRateInfo';
|
||||
});
|
||||
}
|
||||
|
||||
// 检查是否播放完毕
|
||||
if (_parsePosition >= _framePositions.length) {
|
||||
_stopPlaying();
|
||||
setState(() {
|
||||
_statusMessage = '播放完成';
|
||||
_parsePosition = 0;
|
||||
});
|
||||
_addLog('播放完成,已解码 $_frameCount 帧');
|
||||
}
|
||||
} catch (e) {
|
||||
_addLog('解码错误: $e');
|
||||
setState(() {
|
||||
_statusMessage = '解码错误: $e';
|
||||
});
|
||||
_stopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
// 使用原始数据直接解码,每次取一小块
|
||||
Future<void> _decodeRawData() async {
|
||||
try {
|
||||
// 计算当前位置
|
||||
int currentPos = _parsePosition * 1024; // 每次取1KB数据
|
||||
|
||||
// 检查是否到达文件末尾
|
||||
if (currentPos >= _h264Data!.length) {
|
||||
_parsePosition = 0;
|
||||
currentPos = 0;
|
||||
_addLog('原始解码模式:已到达文件末尾,重新开始');
|
||||
}
|
||||
|
||||
// 计算块大小
|
||||
int blockSize = 1024;
|
||||
if (currentPos + blockSize > _h264Data!.length) {
|
||||
blockSize = _h264Data!.length - currentPos;
|
||||
}
|
||||
|
||||
// 提取数据块
|
||||
Uint8List blockData = Uint8List(blockSize);
|
||||
blockData.setRange(0, blockSize, _h264Data!, currentPos);
|
||||
|
||||
// 每10帧记录一下进度
|
||||
if (_frameCount % 10 == 0) {
|
||||
_addLog('原始解码模式:解码块 #$_frameCount, 位置: $currentPos, 大小: $blockSize');
|
||||
}
|
||||
|
||||
// 解码数据块,强制当作I帧
|
||||
bool success =
|
||||
await VideoDecodePlugin.decodeFrame(blockData, FrameType.iFrame);
|
||||
|
||||
// 更新计数
|
||||
_frameCount++;
|
||||
_parsePosition++;
|
||||
|
||||
// 更新状态
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = '原始模式播放: 位置 $currentPos/${_h264Data!.length}, ' +
|
||||
'大小: ${(blockSize / 1024).toStringAsFixed(2)} KB, ' +
|
||||
'${success ? "成功" : "失败"}';
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
_addLog('原始模式解码错误: $e');
|
||||
_stopPlaying();
|
||||
}
|
||||
}
|
||||
|
||||
// 释放解码器资源
|
||||
Future<void> _releaseDecoder() async {
|
||||
_stopPlaying();
|
||||
_statsTimer?.cancel();
|
||||
_statsTimer = null;
|
||||
|
||||
if (!_isInitialized) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
_addLog('开始释放解码器');
|
||||
final bool success = await VideoDecodePlugin.releaseDecoder();
|
||||
setState(() {
|
||||
_isInitialized = !success;
|
||||
_textureId = null;
|
||||
_statusMessage = success ? '解码器已释放' : '解码器释放失败';
|
||||
});
|
||||
_addLog('解码器释放 ${success ? "成功" : "失败"}');
|
||||
} catch (e) {
|
||||
_addLog('释放解码器错误: $e');
|
||||
setState(() {
|
||||
_statusMessage = '释放解码器错误: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopPlaying();
|
||||
_statsTimer?.cancel();
|
||||
_releaseDecoder();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 构建统计信息UI
|
||||
Widget _buildStatsDisplay() {
|
||||
if (_decoderStats.isEmpty) {
|
||||
return const Text('无统计信息');
|
||||
}
|
||||
|
||||
// 从统计信息中提取有用的字段
|
||||
final bool isBuffering = _decoderStats['isBuffering'] ?? false;
|
||||
final int totalFrames = _decoderStats['totalFrames'] ?? 0;
|
||||
final int renderedFrames = _decoderStats['renderedFrames'] ?? 0;
|
||||
final int droppedFrames = _decoderStats['droppedFrames'] ?? 0;
|
||||
final int inputQueueSize = _decoderStats['inputQueueSize'] ?? 0;
|
||||
final int outputQueueSize = _decoderStats['outputQueueSize'] ?? 0;
|
||||
final int bufferFillPercentage = _decoderStats['bufferFillPercentage'] ?? 0;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text('解码状态: ${isBuffering ? "缓冲中" : "播放中"}',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isBuffering ? Colors.orange : Colors.green)),
|
||||
Text('已解码帧: $totalFrames, 渲染帧: $renderedFrames, 丢弃帧: $droppedFrames'),
|
||||
Text(
|
||||
'输入队列: $inputQueueSize, 输出队列: $outputQueueSize, 缓冲填充率: $bufferFillPercentage%'),
|
||||
Text('Flutter接收到的帧数: $_renderedFrameCount, 已解析帧位置: $_parsePosition'),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// 构建日志显示区域
|
||||
Widget _buildLogDisplay() {
|
||||
return Container(
|
||||
height: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: ListView.builder(
|
||||
controller: _logScrollController,
|
||||
itemCount: _logs.length,
|
||||
itemBuilder: (context, index) {
|
||||
return Text(
|
||||
_logs[index],
|
||||
style: TextStyle(
|
||||
color: _logs[index].contains('错误')
|
||||
? Colors.red
|
||||
: _logs[index].contains('警告')
|
||||
? Colors.yellow
|
||||
: Colors.green,
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MaterialApp(
|
||||
theme: ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
visualDensity: VisualDensity.adaptivePlatformDensity,
|
||||
),
|
||||
home: Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('视频解码插件示例'),
|
||||
),
|
||||
body: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// 显示解码的视频,使用Flutter的Texture组件
|
||||
if (_textureId != null)
|
||||
Container(
|
||||
width: _width.toDouble(),
|
||||
height: _height.toDouble(),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Stack(
|
||||
children: [
|
||||
RepaintBoundary(
|
||||
child: Texture(
|
||||
textureId: _textureId!,
|
||||
filterQuality: FilterQuality.medium,
|
||||
),
|
||||
),
|
||||
// 添加一个覆盖层用于触发刷新
|
||||
if (_renderedFrameCount > 0)
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: Opacity(
|
||||
opacity: 0.0,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
key: ValueKey<int>(
|
||||
_renderedFrameCount), // 通过key强制刷新
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: _width.toDouble(),
|
||||
height: _height.toDouble(),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.black,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
'未初始化',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
_statusMessage,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 显示解码统计信息
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(8),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
color: Colors.grey.shade50,
|
||||
),
|
||||
child: _buildStatsDisplay(),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// 显示日志
|
||||
_buildLogDisplay(),
|
||||
const SizedBox(height: 20),
|
||||
// 控制按钮
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
if (!_isInitialized)
|
||||
ElevatedButton(
|
||||
onPressed: _initDecoder,
|
||||
child: const Text('初始化解码器'),
|
||||
)
|
||||
else if (!_isPlaying)
|
||||
ElevatedButton(
|
||||
onPressed: _startPlaying,
|
||||
child: const Text('播放'),
|
||||
)
|
||||
else
|
||||
ElevatedButton(
|
||||
onPressed: _stopPlaying,
|
||||
child: const Text('停止'),
|
||||
),
|
||||
const SizedBox(width: 20),
|
||||
ElevatedButton(
|
||||
onPressed: _isInitialized ? _releaseDecoder : null,
|
||||
child: const Text('释放解码器'),
|
||||
),
|
||||
if (_isInitialized)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 20.0),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
setState(() {}); // 强制重绘
|
||||
_addLog('触发强制刷新');
|
||||
},
|
||||
child: const Text('强制刷新'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
283
example/pubspec.lock
Normal file
@ -0,0 +1,283 @@
|
||||
# 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"
|
||||
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: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.2"
|
||||
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: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.0"
|
||||
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"
|
||||
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"
|
||||
sdks:
|
||||
dart: ">=3.3.4 <4.0.0"
|
||||
flutter: ">=3.3.0"
|
||||
92
example/pubspec.yaml
Normal file
@ -0,0 +1,92 @@
|
||||
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: '>=3.3.4 <4.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: ../
|
||||
|
||||
# 添加所需的依赖项
|
||||
# path_provider: ^2.1.2
|
||||
# permission_handler: ^12.0.0+1
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
cupertino_icons: ^1.0.6
|
||||
|
||||
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: ^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:
|
||||
|
||||
# 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/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
|
||||
38
ios/.gitignore
vendored
Normal file
@ -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
|
||||
0
ios/Assets/.gitkeep
Normal file
19
ios/Classes/VideoDecodePlugin.swift
Normal file
@ -0,0 +1,19 @@
|
||||
import Flutter
|
||||
import UIKit
|
||||
|
||||
public class VideoDecodePlugin: NSObject, FlutterPlugin {
|
||||
public static func register(with registrar: FlutterPluginRegistrar) {
|
||||
let channel = FlutterMethodChannel(name: "video_decode_plugin", binaryMessenger: registrar.messenger())
|
||||
let instance = VideoDecodePlugin()
|
||||
registrar.addMethodCallDelegate(instance, channel: channel)
|
||||
}
|
||||
|
||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
switch call.method {
|
||||
case "getPlatformVersion":
|
||||
result("iOS " + UIDevice.current.systemVersion)
|
||||
default:
|
||||
result(FlutterMethodNotImplemented)
|
||||
}
|
||||
}
|
||||
}
|
||||
23
ios/video_decode_plugin.podspec
Normal file
@ -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
|
||||
362
lib/video_decode_plugin.dart
Normal file
@ -0,0 +1,362 @@
|
||||
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';
|
||||
|
||||
/// 视频帧类型
|
||||
enum FrameType {
|
||||
/// I帧
|
||||
iFrame,
|
||||
|
||||
/// P帧
|
||||
pFrame,
|
||||
}
|
||||
|
||||
/// 视频编码类型
|
||||
enum CodecType {
|
||||
/// H.264编码
|
||||
h264,
|
||||
|
||||
/// H.265编码
|
||||
h265,
|
||||
}
|
||||
|
||||
/// 帧可用回调函数类型
|
||||
typedef FrameAvailableCallback = void Function(int textureId);
|
||||
|
||||
/// 解码器实例内部类
|
||||
class _DecoderInstance {
|
||||
final int textureId;
|
||||
FrameAvailableCallback? frameCallback;
|
||||
|
||||
_DecoderInstance(this.textureId);
|
||||
}
|
||||
|
||||
/// 视频解码器配置
|
||||
class VideoDecoderConfig {
|
||||
/// 视频宽度,默认640
|
||||
final int width;
|
||||
|
||||
/// 视频高度,默认360
|
||||
final int height;
|
||||
|
||||
/// 帧率,可为空
|
||||
final int? frameRate;
|
||||
|
||||
/// 编码类型,默认h264
|
||||
final CodecType codecType;
|
||||
|
||||
/// 缓冲区大小(帧数),默认25帧
|
||||
final int bufferSize;
|
||||
|
||||
/// 解码线程数,默认1线程
|
||||
final int threadCount;
|
||||
|
||||
/// 是否为调试模式,默认false
|
||||
final bool isDebug;
|
||||
|
||||
/// 是否启用硬件解码,默认true
|
||||
final bool enableHardwareDecoder;
|
||||
|
||||
/// 构造函数
|
||||
VideoDecoderConfig({
|
||||
this.width = 640,
|
||||
this.height = 360,
|
||||
this.frameRate,
|
||||
this.codecType = CodecType.h264,
|
||||
this.bufferSize = 25,
|
||||
this.threadCount = 1,
|
||||
this.isDebug = false,
|
||||
this.enableHardwareDecoder = true,
|
||||
});
|
||||
|
||||
/// 转换为Map
|
||||
Map<String, dynamic> toMap() {
|
||||
return {
|
||||
'width': width,
|
||||
'height': height,
|
||||
'frameRate': frameRate,
|
||||
'codecType': codecType.toString().split('.').last,
|
||||
'bufferSize': bufferSize,
|
||||
'threadCount': threadCount,
|
||||
'isDebug': isDebug,
|
||||
'enableHardwareDecoder': enableHardwareDecoder,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// 视频解码插件主类
|
||||
class VideoDecodePlugin {
|
||||
static const MethodChannel _channel = MethodChannel('video_decode_plugin');
|
||||
|
||||
// 解码器映射表,支持多实例
|
||||
static final Map<int, _DecoderInstance> _decoders = {};
|
||||
|
||||
// 默认解码器ID
|
||||
static int? _defaultTextureId;
|
||||
|
||||
// 监听器初始化标志
|
||||
static bool _listenerInitialized = false;
|
||||
|
||||
/// 初始化方法通道监听器
|
||||
static void _initializeMethodCallHandler() {
|
||||
if (!_listenerInitialized) {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
switch (call.method) {
|
||||
case 'onFrameAvailable':
|
||||
final Map<dynamic, dynamic> args = call.arguments;
|
||||
final int textureId = args['textureId'];
|
||||
|
||||
// 调用特定纹理ID的帧回调
|
||||
final decoder = _decoders[textureId];
|
||||
if (decoder != null && decoder.frameCallback != null) {
|
||||
decoder.frameCallback!(textureId);
|
||||
}
|
||||
|
||||
return null;
|
||||
default:
|
||||
throw PlatformException(
|
||||
code: 'Unimplemented',
|
||||
details: 'The method ${call.method} is not implemented',
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
_listenerInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取平台版本
|
||||
static Future<String?> getPlatformVersion() {
|
||||
return VideoDecodePluginPlatform.instance.getPlatformVersion();
|
||||
}
|
||||
|
||||
/// 检查当前平台是否支持
|
||||
static bool get isPlatformSupported {
|
||||
return Platform.isAndroid || Platform.isIOS;
|
||||
}
|
||||
|
||||
/// 设置帧回调(默认解码器)
|
||||
static void setFrameCallback(FrameAvailableCallback callback) {
|
||||
if (_defaultTextureId != null) {
|
||||
setFrameCallbackForTexture(_defaultTextureId!, callback);
|
||||
}
|
||||
}
|
||||
|
||||
/// 为特定纹理ID设置帧回调
|
||||
static void setFrameCallbackForTexture(
|
||||
int textureId, FrameAvailableCallback callback) {
|
||||
_initializeMethodCallHandler();
|
||||
|
||||
final decoder = _decoders[textureId];
|
||||
if (decoder != null) {
|
||||
decoder.frameCallback = callback;
|
||||
}
|
||||
}
|
||||
|
||||
/// 初始化解码器
|
||||
static Future<int?> initDecoder(VideoDecoderConfig config) async {
|
||||
// 先释放之前的默认解码器
|
||||
if (_defaultTextureId != null) {
|
||||
await releaseDecoder();
|
||||
}
|
||||
|
||||
return await createDecoder(config);
|
||||
}
|
||||
|
||||
/// 创建新的解码器实例(支持多实例)
|
||||
static Future<int?> createDecoder(VideoDecoderConfig config) async {
|
||||
if (!isPlatformSupported) {
|
||||
debugPrint('当前平台不支持视频解码插件');
|
||||
return null;
|
||||
}
|
||||
|
||||
// 确保监听器已初始化
|
||||
_initializeMethodCallHandler();
|
||||
|
||||
try {
|
||||
final textureId =
|
||||
await _channel.invokeMethod<int>('initDecoder', config.toMap());
|
||||
|
||||
if (textureId != null) {
|
||||
// 创建新解码器实例并保存
|
||||
final decoder = _DecoderInstance(textureId);
|
||||
_decoders[textureId] = decoder;
|
||||
|
||||
// 设置为默认解码器
|
||||
_defaultTextureId = textureId;
|
||||
}
|
||||
|
||||
return _defaultTextureId;
|
||||
} catch (e) {
|
||||
debugPrint('初始化解码器失败: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取默认纹理ID
|
||||
static int? get textureId => _defaultTextureId;
|
||||
|
||||
/// 获取所有活跃的纹理ID
|
||||
static List<int> get allTextureIds => _decoders.keys.toList();
|
||||
|
||||
/// 解码视频帧(默认解码器)
|
||||
static Future<bool> decodeFrame(
|
||||
Uint8List frameData, FrameType frameType) async {
|
||||
if (_defaultTextureId == null) {
|
||||
debugPrint('解码器未初始化');
|
||||
return false;
|
||||
}
|
||||
|
||||
return decodeFrameForTexture(_defaultTextureId!, frameData, frameType);
|
||||
}
|
||||
|
||||
/// 为特定纹理ID解码视频帧
|
||||
static Future<bool> decodeFrameForTexture(
|
||||
int textureId, Uint8List frameData, FrameType frameType) async {
|
||||
if (!_decoders.containsKey(textureId)) {
|
||||
debugPrint('找不到纹理ID: $textureId');
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return await _channel.invokeMethod<bool>('decodeFrame', {
|
||||
'textureId': textureId,
|
||||
'frameData': frameData,
|
||||
'frameType': frameType.index,
|
||||
}) ??
|
||||
false;
|
||||
} catch (e) {
|
||||
debugPrint('解码帧失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放默认解码器资源
|
||||
static Future<bool> releaseDecoder() async {
|
||||
if (_defaultTextureId == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final result = await releaseDecoderForTexture(_defaultTextureId!);
|
||||
if (result) {
|
||||
_defaultTextureId = null;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 释放特定纹理ID的解码器资源
|
||||
static Future<bool> releaseDecoderForTexture(int textureId) async {
|
||||
if (!_decoders.containsKey(textureId)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
final result = await _channel.invokeMethod<bool>('releaseDecoder', {
|
||||
'textureId': textureId,
|
||||
}) ??
|
||||
false;
|
||||
|
||||
if (result) {
|
||||
// 从映射表中移除
|
||||
_decoders.remove(textureId);
|
||||
|
||||
// 如果释放的是默认解码器,重置默认ID
|
||||
if (_defaultTextureId == textureId) {
|
||||
_defaultTextureId = null;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint('释放解码器失败: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 释放所有解码器
|
||||
static Future<bool> releaseAllDecoders() async {
|
||||
bool allSuccess = true;
|
||||
|
||||
// 复制键列表,因为我们会在迭代过程中修改映射
|
||||
final textureIds = List<int>.from(_decoders.keys);
|
||||
|
||||
// 释放每个解码器
|
||||
for (final textureId in textureIds) {
|
||||
final success = await releaseDecoderForTexture(textureId);
|
||||
if (!success) {
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 清空状态
|
||||
_decoders.clear();
|
||||
_defaultTextureId = null;
|
||||
|
||||
return allSuccess;
|
||||
}
|
||||
|
||||
/// 清除特定纹理ID的回调
|
||||
static void clearCallbackForTexture(int textureId) {
|
||||
final decoder = _decoders[textureId];
|
||||
if (decoder != null) {
|
||||
decoder.frameCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 清除所有回调
|
||||
static void clearAllCallbacks() {
|
||||
for (final decoder in _decoders.values) {
|
||||
decoder.frameCallback = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 注册插件(不需要手动调用)
|
||||
static void registerWith() {
|
||||
// 仅用于插件注册
|
||||
}
|
||||
|
||||
/// 获取解码器统计信息
|
||||
///
|
||||
/// [textureId] 纹理ID
|
||||
/// 返回包含统计信息的Map,包括:
|
||||
/// - totalFramesReceived: 接收的总帧数
|
||||
/// - framesRendered: 成功渲染的帧数
|
||||
/// - framesDropped: 丢弃的帧数
|
||||
/// - lastFrameTimestamp: 最后一帧时间戳
|
||||
/// - averageProcessingTimeMs: 平均处理时间(毫秒)
|
||||
/// - decoderCount: 当前活跃的解码器数量
|
||||
static Future<Map<String, dynamic>> getDecoderStats(int textureId) async {
|
||||
try {
|
||||
final params = {
|
||||
'textureId': textureId,
|
||||
};
|
||||
|
||||
final result = await _channel.invokeMethod<Map<Object?, Object?>>(
|
||||
'getDecoderStats', params);
|
||||
if (result == null) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 将Object?类型转换为明确的类型
|
||||
final Map<String, dynamic> typedResult = {};
|
||||
result.forEach((key, value) {
|
||||
if (key is String) {
|
||||
typedResult[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return typedResult;
|
||||
} catch (e) {
|
||||
if (kDebugMode) {
|
||||
print('获取解码器统计信息失败: $e');
|
||||
}
|
||||
return {};
|
||||
}
|
||||
}
|
||||
}
|
||||
18
lib/video_decode_plugin_method_channel.dart
Normal file
@ -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<String?> getPlatformVersion() async {
|
||||
final version =
|
||||
await methodChannel.invokeMethod<String>('getPlatformVersion');
|
||||
return version;
|
||||
}
|
||||
}
|
||||
30
lib/video_decode_plugin_platform_interface.dart
Normal file
@ -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<String?> getPlatformVersion() {
|
||||
throw UnimplementedError('platformVersion() has not been implemented.');
|
||||
}
|
||||
}
|
||||
72
pubspec.yaml
Normal file
@ -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
|
||||