feat:init

This commit is contained in:
liyi 2025-04-21 10:56:28 +08:00
parent 2517a08b56
commit 3381ef72fc
89 changed files with 4448 additions and 59 deletions

29
.gitignore vendored Normal file
View 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
View 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
View File

@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.

1
LICENSE Normal file
View File

@ -0,0 +1 @@
TODO: Add your license here.

176
README.md
View File

@ -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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
rootProject.name = 'video_decode_plugin'

View 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>

View File

@ -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)
}
}

View File

@ -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)
}
}
}

View File

@ -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
)

View File

@ -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
View 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
View 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.

View 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
View 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

View 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 {}

View 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>

View 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>

View File

@ -0,0 +1,5 @@
package top.skychip.video_decode_plugin_example
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -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>

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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>

View 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>

View 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>

View 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
}

View 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

View 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

View 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

Binary file not shown.

34
example/ios/.gitignore vendored Normal file
View 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

View 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>

View File

@ -0,0 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig"

View 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
View 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

View 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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View 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)
}
}

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View 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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View 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.

View 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>

View 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>

View 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>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View 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)
}
}

View 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++;
// 10I帧
bool isIFrame = _frameCount % (_pFramesPerIFrame + 1) == 1;
List<int> frameData = [];
if (isIFrame) {
// I帧: SPS和PPSI帧数据
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
View 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) {
// 使1024I帧
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
View 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
View 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
View 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
View File

View 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)
}
}
}

View 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

View 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 {};
}
}
}

View 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;
}
}

View 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
View 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