feat(video,android): 视频下载转码播放 + 列表下载/播放按钮

- 集成 ffmpeg-kit-full-gpl 6.0.3(arm64),用 libx264 重编码修复 .h264 缺 SPS/PPS 导致的播放卡死

- 新增 VideoDownloadPlayService/VideoCacheManager,.mp4 直下直播、.h264 下载后转 mp4 缓存,转码完成后删除临时 .h264

- FFmpegKitBridge 走主线程 JNI 调用规避 IL2CPP 后台线程 FindClass 不稳定

- 视频列表按缓存状态显示 下载/播放 按钮,下载中亮色条显示百分比;播放面板原下载按钮改为保存到相册
This commit is contained in:
“虞渠成” 2026-09-08 13:31:31 +08:00
parent 8074ba71c0
commit 4726107e19
55 changed files with 2292 additions and 94 deletions

View File

@ -1,5 +1,6 @@
{
"dotnet.defaultSolution": "Kill.sln",
"dotnet.dotnetPath": "C:/Program Files/dotnet/dotnet.exe",
"dotrush.roslyn.dotnetSdkDirectory": "C:/Program Files/dotnet/sdk/8.0.113",
"dotrush.msbuildProperties": {
"DefineConstants": "UNITY_EDITOR"

View File

@ -0,0 +1,40 @@
package com.arthenica.smartexception.java;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
/**
* smartexception-java 的最小 stub
* ffmpeg-kit 6.0.2 内部 swordfish 库依赖此类的 getStackTraceString / registerRootPackage
* 我们不需要完整功能,只需保证类存在且方法签名匹配
*/
public class Exceptions {
private static final Set<String> rootPackages = Collections.synchronizedSet(new HashSet<String>());
public static void registerRootPackage(String rootPackage) {
if (rootPackage != null) {
rootPackages.add(rootPackage);
}
}
public static String[] getRegisteredRootPackages() {
return rootPackages.toArray(new String[0]);
}
public static void unregisterRootPackage(String rootPackage) {
if (rootPackage != null) {
rootPackages.remove(rootPackage);
}
}
public static String getStackTraceString(Throwable t) {
if (t == null) return "";
StringWriter sw = new StringWriter();
t.printStackTrace(new PrintWriter(sw));
return sw.toString();
}
}

View File

@ -0,0 +1,32 @@
fileFormatVersion: 2
guid: 24b172d3926ad8942a95e2682e783122
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings: {}
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 5bf7b13fff88f954e8e20570135aa4fa
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: df7805ab2d3b7ee4f96106672813ac91
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: fa7f8d684f49aed42bee73ba831c14e3
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 127b1ca475d6c2c4baeba9d0a82ba22f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 216200a08ba02c84a84097cf5d35beeb
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 7650b1a65f114ef42bcdbe9917e8184e
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 7f3a4d77039e3924b93d7d5f41bea59f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 6dbc173a359c19e4f93988bd069836e5
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: false
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 6c879c557a252f94c809b6e4e11e1d8f
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: c79917e539e4a084fbe78be71658249a
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: 4366edb3a69081d499c29a18976d20e2
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,34 @@
fileFormatVersion: 2
guid: fda05a6417969af418657b2bc27116e8
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings:
CPU: ARM64
Is16KbAligned: true
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: c7a31c1ff4b90e64eace12948fd1b59d
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

View File

@ -0,0 +1,32 @@
fileFormatVersion: 2
guid: d3f31159fe0fd444ca012e3a92066482
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
Android: Android
second:
enabled: 1
settings: {}
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
DefaultValueInitialized: true
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1,8 +1,16 @@
apply plugin: 'com.android.library'
**APPLY_PLUGINS**
repositories {
mavenCentral()
google()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// ffmpeg-kit classes.jar( Unity aar classes )
implementation files('libs/ffmpeg-kit-classes.jar')
// jniLibs .so Plugins/Android/jniLibs/ ,Unity APK
// Android Resolver Dependencies Start
implementation 'com.google.android.gms:play-services-base:18.10.0' // Assets/Firebase/Editor/DatabaseDependencies.xml:17
// implementation 'com.google.firebase:firebase-analytics:23.0.0' // Assets/Firebase/Editor/AnalyticsDependencies.xml:13

View File

@ -3,3 +3,9 @@
# 保留 FilePicker 插件类(通过 AndroidJavaClass 反射调用R8 无法检测到使用)
-keep class com.unity.filepicker.** { *; }
# 保留 ffmpeg-kit 全部类(R8 混淆后 ffmpeg-kit 内部 native 调用会失效)
-keep class com.arthenica.ffmpegkit.** { *; }
-keep class com.arthenica.ffmpegkit.**$* { *; }
# 保留 smartexception stub(ffmpeg-kit 内部 swordfish 依赖)
-keep class com.arthenica.smartexception.java.** { *; }

View File

@ -1294,6 +1294,11 @@
"zh": "下载",
"en": "Download"
},
{
"key": "save_to_album",
"zh": "保存到相册",
"en": "Save to album"
},
{
"key": "100257",
"zh": "已保存到相册",

Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: c229daec576184744b211d6dba64b86d
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -1555,8 +1555,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 33.30693, y: -25}
m_SizeDelta: {x: 11.485148, y: 32.158413}
m_AnchoredPosition: {x: 34, y: -25}
m_SizeDelta: {x: 12, y: 33}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &7651326578310032666
CanvasRenderer:
@ -1634,8 +1634,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 70.0594, y: -25}
m_SizeDelta: {x: 62.0198, y: 32.158413}
m_AnchoredPosition: {x: 71, y: -25}
m_SizeDelta: {x: 62, y: 33}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5439455909549482936
CanvasRenderer:
@ -1713,8 +1713,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 13.782178, y: -25}
m_SizeDelta: {x: 27.564356, y: 32.158413}
m_AnchoredPosition: {x: 14, y: -25}
m_SizeDelta: {x: 28, y: 33}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &9138258466124955313
CanvasRenderer:
@ -2407,7 +2407,7 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 268.099, y: -45}
m_AnchoredPosition: {x: 263.5, y: -45}
m_SizeDelta: {x: 48, y: 48}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1825978237086955098
@ -2438,7 +2438,7 @@ MonoBehaviour:
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: d683593328a0cd1439ec851b678d6e53, type: 3}
m_Sprite: {fileID: 21300000, guid: 2a30eeb1d11a4c54ab247ce04ff970dc, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
@ -3199,8 +3199,8 @@ RectTransform:
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 1}
m_AnchorMax: {x: 0, y: 1}
m_AnchoredPosition: {x: 385.0099, y: -45}
m_SizeDelta: {x: 137.82178, y: 90}
m_AnchoredPosition: {x: 381, y: -45}
m_SizeDelta: {x: 139, y: 90}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &3173438462998470296
CanvasRenderer:
@ -3256,7 +3256,7 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 89f1d49b50a31514787b4e268b1c73bb, type: 3}
m_Name:
m_EditorClassIdentifier:
textKey: 100256
textKey: save_to_album
--- !u!1 &9076327582970621402
GameObject:
m_ObjectHideFlags: 0

View File

@ -1,5 +1,80 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!1 &497558733087982060
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 5882713478833418639}
- component: {fileID: 1036065137287717569}
- component: {fileID: 367163259704343791}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &5882713478833418639
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 497558733087982060}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 9188773615237041945}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -50, y: 0}
m_SizeDelta: {x: 100, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &1036065137287717569
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 497558733087982060}
m_CullTransparentMesh: 1
--- !u!114 &367163259704343791
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 497558733087982060}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &877347991771310610
GameObject:
m_ObjectHideFlags: 0
@ -79,6 +154,202 @@ MonoBehaviour:
m_VerticalOverflow: 0
m_LineSpacing: 1
m_Text:
--- !u!1 &2051001936009020673
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 4673987278082270048}
- component: {fileID: 5362905219182314851}
- component: {fileID: 2535855211290697476}
m_Layer: 5
m_Name: fill
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &4673987278082270048
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2051001936009020673}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 8623461907935592638}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0.5, y: 0.5}
m_AnchorMax: {x: 0.5, y: 0.5}
m_AnchoredPosition: {x: 0, y: 0}
m_SizeDelta: {x: 56, y: 56}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5362905219182314851
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2051001936009020673}
m_CullTransparentMesh: 1
--- !u!114 &2535855211290697476
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2051001936009020673}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 0c71bc27e9a8003489c4ecac25d77cf7, type: 3}
m_Type: 3
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 0
m_FillClockwise: 1
m_FillOrigin: 2
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &2132022660410915657
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 636836731591056723}
- component: {fileID: 3523228025851238942}
- component: {fileID: 4899917637513757111}
- component: {fileID: 9005445394137887220}
m_Layer: 5
m_Name: "\u4E0B\u8F7D"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!224 &636836731591056723
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2132022660410915657}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 9065212358823370560}
m_Father: {fileID: 68797095166843205}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0.5}
m_AnchorMax: {x: 1, y: 0.5}
m_AnchoredPosition: {x: -24, y: 0}
m_SizeDelta: {x: 50, y: 50}
m_Pivot: {x: 1, y: 0.5}
--- !u!222 &3523228025851238942
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2132022660410915657}
m_CullTransparentMesh: 1
--- !u!114 &4899917637513757111
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2132022660410915657}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: c229daec576184744b211d6dba64b86d, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &9005445394137887220
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 2132022660410915657}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 4899917637513757111}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &3598736706289518838
GameObject:
m_ObjectHideFlags: 0
@ -91,7 +362,6 @@ GameObject:
- component: {fileID: 5299248103163038637}
- component: {fileID: 2175641447419279724}
- component: {fileID: 9151980594487127953}
- component: {fileID: 6414481814838801359}
m_Layer: 5
m_Name: "\u4E00\u6761\u89C6\u9891"
m_TagString: Untagged
@ -114,6 +384,8 @@ RectTransform:
- {fileID: 3798966534521417101}
- {fileID: 7265590296071766734}
- {fileID: 9188773615237041945}
- {fileID: 636836731591056723}
- {fileID: 8623461907935592638}
m_Father: {fileID: 0}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
@ -174,50 +446,11 @@ MonoBehaviour:
timeText: {fileID: 4285570928671144042}
videoImage: {fileID: 7879863914677697422}
data: {fileID: 4035548829646174203}
--- !u!114 &6414481814838801359
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 3598736706289518838}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 0
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 2175641447419279724}
m_OnClick:
m_PersistentCalls:
m_Calls: []
downloadButton: {fileID: 9005445394137887220}
playButton: {fileID: 8721096871482888230}
loadingImage: {fileID: 293915980322065354}
fillLoadingImage: {fileID: 2535855211290697476}
videoUrl:
--- !u!1 &4411859370997722127
GameObject:
m_ObjectHideFlags: 0
@ -229,8 +462,9 @@ GameObject:
- component: {fileID: 9188773615237041945}
- component: {fileID: 6119825987542687625}
- component: {fileID: 7879863914677697422}
- component: {fileID: 8721096871482888230}
m_Layer: 5
m_Name: "\u9884\u89C8\u56FE"
m_Name: "\u64AD\u653E"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
@ -247,7 +481,8 @@ RectTransform:
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Children:
- {fileID: 5882713478833418639}
m_Father: {fileID: 68797095166843205}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0.5}
@ -293,6 +528,125 @@ MonoBehaviour:
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!114 &8721096871482888230
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 4411859370997722127}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 4e29b1a8efbd4b44bb3f3716e73f07ff, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Navigation:
m_Mode: 3
m_WrapAround: 0
m_SelectOnUp: {fileID: 0}
m_SelectOnDown: {fileID: 0}
m_SelectOnLeft: {fileID: 0}
m_SelectOnRight: {fileID: 0}
m_Transition: 1
m_Colors:
m_NormalColor: {r: 1, g: 1, b: 1, a: 1}
m_HighlightedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_PressedColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 1}
m_SelectedColor: {r: 0.9607843, g: 0.9607843, b: 0.9607843, a: 1}
m_DisabledColor: {r: 0.78431374, g: 0.78431374, b: 0.78431374, a: 0.5019608}
m_ColorMultiplier: 1
m_FadeDuration: 0.1
m_SpriteState:
m_HighlightedSprite: {fileID: 0}
m_PressedSprite: {fileID: 0}
m_SelectedSprite: {fileID: 0}
m_DisabledSprite: {fileID: 0}
m_AnimationTriggers:
m_NormalTrigger: Normal
m_HighlightedTrigger: Highlighted
m_PressedTrigger: Pressed
m_SelectedTrigger: Selected
m_DisabledTrigger: Disabled
m_Interactable: 1
m_TargetGraphic: {fileID: 7879863914677697422}
m_OnClick:
m_PersistentCalls:
m_Calls: []
--- !u!1 &5010573327037882523
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 9065212358823370560}
- component: {fileID: 5849985704768591495}
- component: {fileID: 887128703327512524}
m_Layer: 5
m_Name: Image
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 1
--- !u!224 &9065212358823370560
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5010573327037882523}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children: []
m_Father: {fileID: 636836731591056723}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 0, y: 0}
m_AnchorMax: {x: 1, y: 1}
m_AnchoredPosition: {x: -50, y: 0}
m_SizeDelta: {x: 100, y: 50}
m_Pivot: {x: 0.5, y: 0.5}
--- !u!222 &5849985704768591495
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5010573327037882523}
m_CullTransparentMesh: 1
--- !u!114 &887128703327512524
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 5010573327037882523}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 0}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 0}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 0
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1
--- !u!1 &7520780073196005562
GameObject:
m_ObjectHideFlags: 0
@ -372,3 +726,79 @@ MonoBehaviour:
m_VerticalOverflow: 0
m_LineSpacing: 1.3
m_Text:
--- !u!1 &8869706884894642105
GameObject:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
serializedVersion: 6
m_Component:
- component: {fileID: 8623461907935592638}
- component: {fileID: 5473811544253790937}
- component: {fileID: 293915980322065354}
m_Layer: 5
m_Name: "\u8FDB\u5EA6"
m_TagString: Untagged
m_Icon: {fileID: 0}
m_NavMeshLayer: 0
m_StaticEditorFlags: 0
m_IsActive: 0
--- !u!224 &8623461907935592638
RectTransform:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8869706884894642105}
m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
m_LocalPosition: {x: 0, y: 0, z: 0}
m_LocalScale: {x: 1, y: 1, z: 1}
m_ConstrainProportionsScale: 0
m_Children:
- {fileID: 4673987278082270048}
m_Father: {fileID: 68797095166843205}
m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
m_AnchorMin: {x: 1, y: 0.5}
m_AnchorMax: {x: 1, y: 0.5}
m_AnchoredPosition: {x: -24, y: 0}
m_SizeDelta: {x: 56, y: 56}
m_Pivot: {x: 1, y: 0.5}
--- !u!222 &5473811544253790937
CanvasRenderer:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8869706884894642105}
m_CullTransparentMesh: 1
--- !u!114 &293915980322065354
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 8869706884894642105}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: fe87c0e1cc204ed48ad3b37840f39efc, type: 3}
m_Name:
m_EditorClassIdentifier:
m_Material: {fileID: 0}
m_Color: {r: 1, g: 1, b: 1, a: 1}
m_RaycastTarget: 1
m_RaycastPadding: {x: 0, y: 0, z: 0, w: 0}
m_Maskable: 1
m_OnCullStateChanged:
m_PersistentCalls:
m_Calls: []
m_Sprite: {fileID: 21300000, guid: 85008df8bc29d934bad661ef65c1943c, type: 3}
m_Type: 0
m_PreserveAspect: 0
m_FillCenter: 1
m_FillMethod: 4
m_FillAmount: 1
m_FillClockwise: 1
m_FillOrigin: 2
m_UseSpriteMesh: 0
m_PixelsPerUnitMultiplier: 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 2a30eeb1d11a4c54ab247ce04ff970dc
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 0c71bc27e9a8003489c4ecac25d77cf7
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -0,0 +1,127 @@
fileFormatVersion: 2
guid: 85008df8bc29d934bad661ef65c1943c
TextureImporter:
internalIDToNameTable: []
externalObjects: {}
serializedVersion: 13
mipmaps:
mipMapMode: 0
enableMipMap: 0
sRGBTexture: 1
linearTexture: 0
fadeOut: 0
borderMipMap: 0
mipMapsPreserveCoverage: 0
alphaTestReferenceValue: 0.5
mipMapFadeDistanceStart: 1
mipMapFadeDistanceEnd: 3
bumpmap:
convertToNormalMap: 0
externalNormalMap: 0
heightScale: 0.25
normalMapFilter: 0
flipGreenChannel: 0
isReadable: 0
streamingMipmaps: 0
streamingMipmapsPriority: 0
vTOnly: 0
ignoreMipmapLimit: 0
grayScaleToAlpha: 0
generateCubemap: 6
cubemapConvolution: 0
seamlessCubemap: 0
textureFormat: 1
maxTextureSize: 2048
textureSettings:
serializedVersion: 2
filterMode: 1
aniso: 1
mipBias: 0
wrapU: 1
wrapV: 1
wrapW: 0
nPOTScale: 0
lightmap: 0
compressionQuality: 50
spriteMode: 1
spriteExtrude: 1
spriteMeshType: 1
alignment: 0
spritePivot: {x: 0.5, y: 0.5}
spritePixelsToUnits: 100
spriteBorder: {x: 0, y: 0, z: 0, w: 0}
spriteGenerateFallbackPhysicsShape: 1
alphaUsage: 1
alphaIsTransparency: 1
spriteTessellationDetail: -1
textureType: 8
textureShape: 1
singleChannelComponent: 0
flipbookRows: 1
flipbookColumns: 1
maxTextureSizeSet: 0
compressionQualitySet: 0
textureFormatSet: 0
ignorePngGamma: 0
applyGammaDecoding: 0
swizzle: 50462976
cookieLightType: 0
platformSettings:
- serializedVersion: 3
buildTarget: DefaultTexturePlatform
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Standalone
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
- serializedVersion: 3
buildTarget: Android
maxTextureSize: 2048
resizeAlgorithm: 0
textureFormat: -1
textureCompression: 1
compressionQuality: 50
crunchedCompression: 0
allowsAlphaSplitting: 0
overridden: 0
ignorePlatformSupport: 0
androidETC2FallbackOverride: 0
forceMaximumCompressionQuality_BC6H_BC7: 0
spriteSheet:
serializedVersion: 2
sprites: []
outline: []
physicsShape: []
bones: []
spriteID: 5e97eb03825dee720800000000000000
internalID: 0
vertices: []
indices:
edges: []
weights: []
secondaryTextures: []
nameFileIdTable: {}
mipmapLimitGroupName:
pSDRemoveMatte: 0
userData:
assetBundleName:
assetBundleVariant:

View File

@ -170,6 +170,7 @@ namespace Kill.UI.Pages
{
foreach (var item in data.data.records)
{
Debug.Log(totalCount);
KillData kd = new KillData()
{
@ -178,7 +179,7 @@ namespace Kill.UI.Pages
dis=item.distance
};
CreateKillItem(kd);
Debug.Log(totalCount);
totalCount++;
@ -226,8 +227,12 @@ namespace Kill.UI.Pages
if (videoUIItemPrefab == null || content == null) return;
var item = Instantiate(videoUIItemPrefab, content);
item.GetComponent<VideoUIItem>().Init(data.time, data.coverUrl);
item.GetComponent<Button>().onClick.AddListener(() => ClickOneVideo(data.videoUrl));
var comp = item.GetComponent<VideoUIItem>();
comp.Init(data.time, data.coverUrl);
comp.SetupVideo(data.videoUrl,
onDownload: () => OnItemDownload(comp),
onPlay: () => OnItemPlay(comp));
//item.GetComponent<Button>().onClick.AddListener(() => OnItemRowClick(comp));
videoList.Add(item.gameObject);
}
/// <summary>
@ -243,35 +248,84 @@ namespace Kill.UI.Pages
videoList.Add(item.gameObject);
}
public VideoCtrl videoCtrl;
public void ClickOneVideo(string url)
public Button downloadBtn;
/// <summary>用本地 mp4 打开播放器面板,并把"保存到相册"按钮绑定到该本地文件</summary>
private void OpenPlayer(string path)
{
if (videoCtrl == null) { ToastUI.Show("100332"); return; }
videoCtrl.transform.parent.gameObject.SetActive(true);
videoCtrl.Init(url,null);
downloadBtn.onClick.RemoveAllListeners();
downloadBtn.onClick.AddListener(() => ClickDownload(url));
videoCtrl.Init(path, null);
if (downloadBtn != null)
{
downloadBtn.onClick.RemoveAllListeners();
downloadBtn.onClick.AddListener(() => SaveVideoToGallery(path, Path.GetFileName(path)));
}
}
public void CloseVideo()
{
videoCtrl.transform.parent.gameObject.SetActive(false);
videoCtrl.Stop();
}
public async Task DownLoadVideo(string url)
{
UpdateLoadingUI.Instance.Show();
string name = url.Substring(url.LastIndexOf('/') + 1);
string savePath = Application.persistentDataPath + "/"+name;
var success = await NetworkCtrl.Instance.DownloadFile(url, savePath, UpdateLoadingUI.Instance.GetDownloadProcess);
UpdateLoadingUI.Instance.Hide();
if (success.IsSuccess && File.Exists(savePath))
{
// 下载成功,保存到系统相册
SaveVideoToGallery(savePath, name);
}
// ============ 列表项:下载 / 播放 逻辑 ============
/// <summary>点 item 下载按钮:仅下载(+h264 转码),完成后刷新按钮为"播放"</summary>
public void OnItemDownload(VideoUIItem item)
{
if (item == null || string.IsNullOrEmpty(item.videoUrl)) return;
item.ShowDownloading(true);
new Kill.Video.VideoDownloadPlayService().DownloadConvertAndPlay(
item.videoUrl,
onDownloadProgress: p => item.UpdateDownloadProgress(false, p),
onConvertStart: null,
onComplete: (success, path) =>
{
item.UpdateDownloadProgress(true, null);
if (success) item.RefreshButtons();
else ToastUI.Show("100258");
});
}
/// <summary>点 item 播放按钮:播本地缓存;未缓存则先下载+转码再播</summary>
public void OnItemPlay(VideoUIItem item)
{
if (item == null || string.IsNullOrEmpty(item.videoUrl)) return;
string path = Kill.Video.VideoCacheManager.GetCachedMp4Path(item.videoUrl);
if (File.Exists(path) && new FileInfo(path).Length > 0)
OpenPlayer(path);
else
{
ToastUI.Show("100258");
}
OnItemDownloadAndPlay(item);
}
/// <summary>点击 item 整行:已下载直接播,未下载先下载(+转码)再播</summary>
public void OnItemRowClick(VideoUIItem item)
{
if (item == null || string.IsNullOrEmpty(item.videoUrl)) return;
if (Kill.Video.VideoCacheManager.IsCached(item.videoUrl))
OpenPlayer(Kill.Video.VideoCacheManager.GetCachedMp4Path(item.videoUrl));
else
OnItemDownloadAndPlay(item);
}
private void OnItemDownloadAndPlay(VideoUIItem item)
{
if (item == null || string.IsNullOrEmpty(item.videoUrl)) return;
item.ShowDownloading(true);
new Kill.Video.VideoDownloadPlayService().DownloadConvertAndPlay(
item.videoUrl,
onDownloadProgress: p => item.UpdateDownloadProgress(false, p),
onConvertStart: null,
onComplete: (success, path) =>
{
item.UpdateDownloadProgress(true, null);
item.RefreshButtons();
if (success && !string.IsNullOrEmpty(path) && File.Exists(path))
OpenPlayer(path);
else
ToastUI.Show("100258");
});
}
/// <summary>
@ -295,15 +349,6 @@ namespace Kill.UI.Pages
});
#endif
}
public Button downloadBtn;
public void ClickDownload(string url)
{
DownLoadVideo(url);
}
public void GetDownloadProcess( DownloadProgress progress)
{
Debug.Log(progress.ProgressPercentage + "%");
}
}

View File

@ -12,13 +12,50 @@ namespace Kill.UI.Pages
public Text timeText;
public Image videoImage;
public Text data;
/// <summary>视频项:未下载时显示下载按钮(prefab 内挂载)</summary>
public Button downloadButton;
/// <summary>视频项:已下载时显示播放按钮(prefab 内挂载)</summary>
public Button playButton;
/// <summary>下载进度条底色(显示/隐藏用)</summary>
public Image loadingImage;
/// <summary>亮色进度条(loadingImage 的子物体,Image type=Filled,进度填充到它身上)</summary>
public Image fillLoadingImage;
public string videoUrl;
bool isVideo = false;
private Image FillBar => fillLoadingImage != null ? fillLoadingImage : loadingImage;
/// <summary>显示/隐藏下载进度条,开始时归零;下载中隐藏 下载/播放 按钮,完成后由 RefreshButtons 恢复</summary>
public void ShowDownloading(bool show)
{
if (loadingImage != null) loadingImage.gameObject.SetActive(show);
var bar = FillBar;
if (show && bar != null) bar.fillAmount = 0f;
// 下载期间收起 下载/播放 按钮,只留进度条
if (downloadButton != null) downloadButton.gameObject.SetActive(false);
if (playButton != null) playButton.gameObject.SetActive(false);
}
/// <summary>用下载进度刷新亮色条 fillAmount(0~1),并在完成时隐藏底色</summary>
public void UpdateDownloadProgress(bool done, Kill.Network.DownloadProgress progress)
{
if (done)
{
if (loadingImage != null) loadingImage.gameObject.SetActive(false);
return;
}
var bar = FillBar;
if (bar == null) return;
float pct = progress != null ? progress.ProgressPercentage : 0f;
bar.fillAmount = Mathf.Clamp01(pct / 100f);
}
public void Init(DateTime time,string path="",float angle=0,float dis=0)
{
timeText.text = time.ToString("yyyy-MM-dd HH:mm:ss");
if(path!="")
{
videoImage.gameObject.SetActive(true);
GetComponent<Button>().interactable=true;
//NetworkCtrl.Instance.LoadImageToUIImageAsync(path, videoImage);
data.text="";
}
@ -36,10 +73,49 @@ namespace Kill.UI.Pages
disStr=lensFt+"ft";
}
videoImage.gameObject.SetActive(false);
GetComponent<Button>().interactable=false;
data.text=$"{angle}° {disStr}";
}
}
/// <summary>
/// 视频项专用:绑定 url 与 下载/播放 两个按钮的回调
/// </summary>
/// <param name="url">视频源 url(.mp4 或 .h264)</param>
/// <param name="onDownload">点下载按钮</param>
/// <param name="onPlay">点播放按钮</param>
public void SetupVideo(string url, UnityEngine.Events.UnityAction onDownload, UnityEngine.Events.UnityAction onPlay)
{
videoUrl = url;
isVideo = true;
if (downloadButton != null)
{
downloadButton.onClick.RemoveAllListeners();
downloadButton.onClick.AddListener(onDownload);
}
if (playButton != null)
{
playButton.onClick.RemoveAllListeners();
playButton.onClick.AddListener(onPlay);
}
RefreshButtons();
}
/// <summary>
/// 根据下载状态刷新按钮显示:未下载显下载、已下载显播放;非视频项隐藏两个按钮
/// </summary>
public void RefreshButtons()
{
if (downloadButton == null || playButton == null) return;
if (!isVideo)
{
downloadButton.gameObject.SetActive(false);
playButton.gameObject.SetActive(false);
return;
}
bool cached = Kill.Video.VideoCacheManager.IsCached(videoUrl);
downloadButton.gameObject.SetActive(!cached);
playButton.gameObject.SetActive(cached);
}
}
}

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e42067f07639a5443b94ceff03670e62
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,263 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Kill.Utils;
using UnityEngine;
namespace Kill.Video
{
/// <summary>
/// ffmpeg-kit-maintained(维护版,群组 dev.ffmpegkit-maintained)C# 桥接
/// 用 AndroidJNI 底层 API 直接调,绕开 AndroidJavaClass.CallStatic 的 Unity bug(Ljava.lang.Object)
/// .264 -> .mp4 用 libx264 重编码(full-gpl 版),补齐干净的 SPS/PPS + 时间戳,
/// 供各手机(含 OPPO/MTK)解码器正常播放;纯 copy 会复制坏帧导致播放卡死。
/// 注意:所有 JNI 调用必须在 Unity 主线程执行(IL2CPP 工作线程 JVM attach 不可靠,FindClass
/// 偶发失败);现依赖为 ffmpeg-kit-full-gpl(含 libx264),free 版没有该编码器。
/// </summary>
public static class FFmpegKitBridge
{
#if UNITY_ANDROID && !UNITY_EDITOR
/// <summary>
/// 异步 .264 -> .mp4 纯封装
/// </summary>
public static Task<bool> ConvertH264ToMp4Async(string inputPath, string outputPath)
{
if (string.IsNullOrEmpty(inputPath) || !File.Exists(inputPath))
return Task.FromResult(false);
if (File.Exists(outputPath))
{
try { File.Delete(outputPath); } catch { }
}
// 设备产出的 .264 裸流 SPS/PPS 引用损坏,纯 -c:v copy 会复制坏帧导致
// 播放器解码卡死("non-existing PPS 0 referenced")。必须是重编码(libx264)
// 重新生成干净的 SPS/PPS + 时间戳,等价 service 端 VideoTranscodeService。
// 注意:需要 ffmpeg-kit-full(含 libx264);free 版无 libx264 编码器会失败。
// preset ultrafast 更快速;crf 值控制质量,视频短可接受
string[] args = new string[] {
"-y",
"-loglevel", "quiet",
"-fflags", "+genpts",
"-i", inputPath,
"-c:v", "libx264",
"-preset", "ultrafast",
"-crf", "23",
"-pix_fmt", "yuv420p",
"-movflags", "+faststart",
outputPath
};
Debug.Log("[FFmpegKitBridge] args: " + string.Join(" ", args));
return Task.Run(() =>
{
bool result = false;
// JNI 必须在主线程执行,否则 FindClass 偶发失败("类未找到")
MainThreadDispatcher.RunOnMainThread(() => result = RunFfmpegConvert(args, outputPath));
return result;
});
}
/// <summary>在 Unity 主线程执行 ffmpeg JNI 调用(同步,阻塞)</summary>
private static bool RunFfmpegConvert(string[] args, string outputPath)
{
try
{
using (var ffkitClass = new AndroidJavaClass("com.arthenica.ffmpegkit.FFmpegKit"))
{
IntPtr classPtr = ffkitClass.GetRawClass();
if (classPtr == IntPtr.Zero)
{
Debug.LogError("[FFmpegKitBridge] FFmpegKit 类未找到(类不在 classpath,多半是装了旧 APK)");
return false;
}
// 主动 FindClass 二重确认类存在(区分"类缺失" vs "方法签名不匹配")
IntPtr findCls = AndroidJNI.FindClass("com/arthenica/ffmpegkit/FFmpegKit");
Debug.Log("[FFmpegKitBridge] FindClass => " + (findCls == IntPtr.Zero ? "NOT_FOUND" : "OK"));
// ffmpeg-kit 6.0+ 用 executeWithArguments(String[]) 而不是 execute(String)
IntPtr executeMethodID = AndroidJNI.GetStaticMethodID(
classPtr,
"executeWithArguments",
"([Ljava/lang/String;)Lcom/arthenica/ffmpegkit/FFmpegSession;");
if (executeMethodID == IntPtr.Zero)
{
Debug.LogError("[FFmpegKitBridge] executeMethodID == 0:类存在但方法 executeWithArguments 未找到(签名不匹配)");
return false;
}
// 构造 String[] 作为 JNI 参数
IntPtr argsArr = AndroidJNI.NewObjectArray(args.Length,
AndroidJNI.FindClass("java/lang/String"), IntPtr.Zero);
for (int i = 0; i < args.Length; i++)
{
IntPtr strPtr = AndroidJNI.NewStringUTF(args[i]);
AndroidJNI.SetObjectArrayElement(argsArr, i, strPtr);
AndroidJNI.DeleteLocalRef(strPtr);
}
jvalue[] jargs = new jvalue[1];
jargs[0].l = argsArr;
IntPtr sessionPtr = AndroidJNI.CallStaticObjectMethod(
classPtr, executeMethodID, jargs);
AndroidJNI.DeleteLocalRef(argsArr);
if (sessionPtr == IntPtr.Zero)
{
Debug.LogError("[FFmpegKitBridge] execute return null");
return false;
}
try { return ReadSessionResult(sessionPtr, outputPath); }
finally { AndroidJNI.DeleteLocalRef(sessionPtr); }
}
}
catch (Exception e)
{
Debug.LogError("[FFmpegKitBridge] execute fail: " + e);
return false;
}
}
/// <summary>读取 FFmpegSession 结果并解析成功与否</summary>
private static bool ReadSessionResult(IntPtr sessionPtr, string outputPath)
{
try
{
// 读 ReturnCode
IntPtr getReturnCodeMID = AndroidJNI.GetMethodID(
AndroidJNI.GetObjectClass(sessionPtr),
"getReturnCode", "()Lcom/arthenica/ffmpegkit/ReturnCode;");
IntPtr rcPtr = AndroidJNI.CallObjectMethod(sessionPtr, getReturnCodeMID, new jvalue[0]);
if (rcPtr == IntPtr.Zero)
{
Debug.LogError("[FFmpegKitBridge] returnCode null");
return false;
}
// ReturnCode.getValue() int
IntPtr rcClassPtr = AndroidJNI.GetObjectClass(rcPtr);
IntPtr getValueMID = AndroidJNI.GetMethodID(rcClassPtr, "getValue", "()I");
int rcValue = AndroidJNI.CallIntMethod(rcPtr, getValueMID, new jvalue[0]);
// ReturnCode.isValueSuccess() boolean
IntPtr isSuccessMID = AndroidJNI.GetMethodID(rcClassPtr, "isValueSuccess", "()Z");
bool isSuccess = AndroidJNI.CallBooleanMethod(rcPtr, isSuccessMID, new jvalue[0]);
Debug.Log("[FFmpegKitBridge] rc=" + rcValue + " isSuccess=" + isSuccess);
// ffmpeg-kit 6.0.2 在某些场景下 rc=255 (SIGURG) 但 ffmpeg 已写完 mp4
// 兜底:同时检查输出文件存在 + 大小 > 0
if (!isSuccess)
{
try
{
if (File.Exists(outputPath))
{
var fi = new FileInfo(outputPath);
if (fi.Length > 0)
{
Debug.LogWarning("[FFmpegKitBridge] ffmpeg exit signal 但 mp4 已生成 (" + fi.Length + " bytes),按成功处理");
isSuccess = true;
}
}
}
catch { }
}
// 失败时打印 ffmpeg 错误日志
if (!isSuccess)
{
try
{
IntPtr sessionClass = AndroidJNI.GetObjectClass(sessionPtr);
IntPtr failStackMID = AndroidJNI.GetMethodID(sessionClass, "getFailStackTrace", "()Ljava/lang/String;");
string failStack = AndroidJNI.CallStringMethod(sessionPtr, failStackMID, new jvalue[0]);
if (!string.IsNullOrEmpty(failStack))
Debug.LogError("[FFmpegKitBridge] failStack: " + failStack);
}
catch { }
try
{
IntPtr sessionClass = AndroidJNI.GetObjectClass(sessionPtr);
IntPtr logsMID = AndroidJNI.GetMethodID(sessionClass, "getAllLogsAsString", "()Ljava/lang/String;");
string logs = AndroidJNI.CallStringMethod(sessionPtr, logsMID, new jvalue[0]);
if (!string.IsNullOrEmpty(logs))
Debug.LogError("[FFmpegKitBridge] allLogs: " + logs);
}
catch { }
}
AndroidJNI.DeleteLocalRef(rcPtr);
return isSuccess;
}
catch (Exception e)
{
Debug.LogError("[FFmpegKitBridge] read result fail: " + e);
return false;
}
}
#region debug-point vf-1 (: mp4 //profile/level,"截断文件 / 时间戳错误 / 解码不支持")
/// <summary>
/// 用 ffprobe 读取 mp4 的媒体信息并以 JSON 原文打日志(证据收集,临时插桩)
/// </summary>
public static void ProbeMp4Info(string path)
{
if (string.IsNullOrEmpty(path) || !File.Exists(path)) return;
MainThreadDispatcher.RunOnMainThread(() =>
{
try
{
string[] args = new string[]
{
"-v", "error",
"-show_entries", "format=duration,size,bit_rate:stream=codec_name,profile,level,width,height,r_frame_rate,avg_frame_rate,nb_frames,time_base,start_time",
"-of", "json", path
};
using (var probeClass = new AndroidJavaClass("com.arthenica.ffmpegkit.FFprobeKit"))
{
IntPtr classPtr = probeClass.GetRawClass();
IntPtr mID = AndroidJNI.GetStaticMethodID(
classPtr,
"executeWithArguments",
"([Ljava/lang/String;)Lcom/arthenica/ffmpegkit/FFprobeSession;");
if (mID == IntPtr.Zero) { Debug.LogError("[ProbeMp4Info] no executeWithArguments"); return; }
IntPtr argsArr = AndroidJNI.NewObjectArray(args.Length,
AndroidJNI.FindClass("java/lang/String"), IntPtr.Zero);
for (int i = 0; i < args.Length; i++)
{
IntPtr s = AndroidJNI.NewStringUTF(args[i]);
AndroidJNI.SetObjectArrayElement(argsArr, i, s);
AndroidJNI.DeleteLocalRef(s);
}
jvalue[] ja = new jvalue[1]; ja[0].l = argsArr;
IntPtr sessionPtr = AndroidJNI.CallStaticObjectMethod(classPtr, mID, ja);
AndroidJNI.DeleteLocalRef(argsArr);
if (sessionPtr == IntPtr.Zero) { Debug.LogError("[ProbeMp4Info] session null"); return; }
try
{
IntPtr cls = AndroidJNI.GetObjectClass(sessionPtr);
IntPtr getOutMID = AndroidJNI.GetMethodID(cls, "getOutput", "()Ljava/lang/String;");
string outJson = AndroidJNI.CallStringMethod(sessionPtr, getOutMID, new jvalue[0]);
Debug.Log("[ProbeMp4Info] ffprobe json: " + (outJson ?? "null"));
}
finally { AndroidJNI.DeleteLocalRef(sessionPtr); }
}
}
catch (Exception e) { Debug.LogError("[ProbeMp4Info] fail: " + e); }
});
}
#endregion
#else
public static Task<bool> ConvertH264ToMp4Async(string inputPath, string outputPath)
{
return Task.FromResult(false);
}
#endif
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 889748d8e0ed12948bb5bca9d84174c3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,67 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using UnityEngine;
namespace Kill.Video
{
/// <summary>
/// 把任意 Action 排到 Unity 主线程执行。
/// 背景:IL2CPP 的自建后台线程 JVM attach 不可靠,AndroidJavaClass/AndroidJNI
/// 在这些线程上 FindClass 会偶发返回空,导致"类未找到/偶尔成功偶尔失败"。
/// 用它在主线程上跑所有 JNI 调用即可稳定。
/// </summary>
public static class MainThreadDispatcher
{
private static readonly ConcurrentQueue<Action> _pending = new ConcurrentQueue<Action>();
private static int _mainThreadId = -1;
private static bool _ensured;
[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
private static void Ensure()
{
if (_ensured) return;
_mainThreadId = Thread.CurrentThread.ManagedThreadId;
var go = new GameObject("MainThreadDispatcher");
go.hideFlags = HideFlags.HideAndDontSave;
UnityEngine.Object.DontDestroyOnLoad(go);
go.AddComponent<DispatcherBehaviour>();
_ensured = true;
}
public static bool IsMainThread =>
!_ensured || Thread.CurrentThread.ManagedThreadId == _mainThreadId;
/// <summary>在主线程执行 action;若调用方已是主线程则直接内联执行。</summary>
public static void RunOnMainThread(Action action)
{
if (action == null) return;
if (IsMainThread)
{
action();
return;
}
using (var done = new ManualResetEventSlim(false))
{
_pending.Enqueue(() =>
{
try { action(); }
finally { done.Set(); }
});
done.Wait();
}
}
private class DispatcherBehaviour : MonoBehaviour
{
private void Update()
{
while (_pending.TryDequeue(out var a))
{
try { a(); }
catch (Exception e) { Debug.LogException(e); }
}
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0156e0cc83e452440aea1ea2ceb09be4
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,64 @@
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using UnityEngine;
namespace Kill.Video
{
/// <summary>
/// 视频缓存管理:按 url 哈希缓存到 persistentDataPath/video_cache
/// key=mp4(转码后),key=tmp.h264(下载中临时)
/// </summary>
public static class VideoCacheManager
{
private const string CacheSubDir = "video_cache";
private static string CacheDir
{
get
{
string dir = Path.Combine(Application.persistentDataPath, CacheSubDir);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
return dir;
}
}
/// <summary>已转码好的 mp4 缓存路径</summary>
public static string GetCachedMp4Path(string url)
{
return Path.Combine(CacheDir, Hash(url) + ".mp4");
}
/// <summary>下载中的 .h264 临时路径</summary>
public static string GetTempH264Path(string url)
{
return Path.Combine(CacheDir, Hash(url) + ".h264");
}
public static bool IsCached(string url)
{
var p = GetCachedMp4Path(url);
return File.Exists(p) && new FileInfo(p).Length > 0;
}
public static void Clear()
{
string dir = Path.Combine(Application.persistentDataPath, CacheSubDir);
if (Directory.Exists(dir))
{
try { Directory.Delete(dir, true); }
catch (Exception e) { Debug.LogWarning("[VideoCacheManager] clear fail: " + e.Message); }
}
}
private static string Hash(string s)
{
using var sha = SHA1.Create();
var bytes = sha.ComputeHash(Encoding.UTF8.GetBytes(s));
var sb = new StringBuilder(bytes.Length * 2);
foreach (var b in bytes) sb.Append(b.ToString("x2"));
return sb.ToString();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a2e512329ac2d2140807f2f1535d9ef1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,152 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Kill.Network;
using UnityEngine;
namespace Kill.Video
{
/// <summary>
/// 下载 .264 -> 转封装 MP4(Android MediaCodec+MediaMuxer) -> 返回本地 mp4 路径
/// 不直接控制 VideoPlayer,由调用方拿到路径后自行 Init
/// </summary>
public class VideoDownloadPlayService
{
/// <summary>
/// 完整流程:下载 → 转码
/// </summary>
/// <param name="url">视频源 url(.264 裸流)</param>
/// <param name="onDownloadProgress">下载进度回调(0~1)</param>
/// <param name="onConvertStart">下载完成,即将开始转码(可更新 UI 提示)</param>
/// <param name="onComplete">(success, mp4Path) success=false 时 mp4Path 为 null/错误信息</param>
public async Task<bool> DownloadConvertAndPlay(
string url,
Action<DownloadProgress> onDownloadProgress,
Action<string> onConvertStart,
Action<bool, string> onComplete)
{
if (string.IsNullOrEmpty(url))
{
onComplete?.Invoke(false, "url is empty");
return false;
}
string mp4Path = VideoCacheManager.GetCachedMp4Path(url);
// 1) 已缓存(mp4 直接缓存 / h264 转码后缓存),直接返回
if (File.Exists(mp4Path) && new FileInfo(mp4Path).Length > 0)
{
onComplete?.Invoke(true, mp4Path);
return true;
}
// 2a) 链接是 .mp4:直接下载即得,无需转码
if (!IsH264Url(url))
{
SafeDelete(mp4Path);
HttpResponse<string> respMp4 = null;
try
{
respMp4 = await NetworkCtrl.Instance.DownloadFile(url, mp4Path, onDownloadProgress);
}
catch (Exception e)
{
Debug.LogError("[VideoDownloadPlayService] mp4 download exception: " + e);
SafeDelete(mp4Path);
onComplete?.Invoke(false, e.Message);
return false;
}
if (respMp4 == null || !respMp4.IsSuccess || !File.Exists(mp4Path) || new FileInfo(mp4Path).Length <= 0)
{
SafeDelete(mp4Path);
onComplete?.Invoke(false, "mp4 download fail: " + (respMp4?.ErrorMessage ?? "unknown"));
return false;
}
onComplete?.Invoke(true, mp4Path);
return true;
}
// 2b) 链接是 .h264:先下载再转码
string h264Path = VideoCacheManager.GetTempH264Path(url);
SafeDelete(h264Path);
SafeDelete(mp4Path);
HttpResponse<string> resp = null;
try
{
resp = await NetworkCtrl.Instance.DownloadFile(url, h264Path, onDownloadProgress);
}
catch (Exception e)
{
Debug.LogError("[VideoDownloadPlayService] download exception: " + e);
SafeDelete(h264Path);
onComplete?.Invoke(false, e.Message);
return false;
}
if (resp == null || !resp.IsSuccess || !File.Exists(h264Path))
{
SafeDelete(h264Path);
onComplete?.Invoke(false, "download fail: " + (resp?.ErrorMessage ?? "unknown"));
return false;
}
onConvertStart?.Invoke(h264Path);
// 3) 转码 .264 -> .mp4(ffmpeg-kit,libx264 重编码)
bool convertOk = await FFmpegKitBridge.ConvertH264ToMp4Async(h264Path, mp4Path);
if (!convertOk)
{
Debug.LogError("[VideoDownloadPlayService] ffmpeg convert fail");
}
#region debug-point vf-2 (: / + mp4 )
try
{
var hi = new FileInfo(h264Path);
var mi = new FileInfo(mp4Path);
Debug.Log("[vf-2] h264Size=" + (hi.Exists ? hi.Length.ToString() : "missing")
+ " mp4Size=" + (mi.Exists ? mi.Length.ToString() : "missing")
+ " convertOk=" + convertOk);
if (mi.Exists && mi.Length > 0)
{
#if UNITY_ANDROID && !UNITY_EDITOR
FFmpegKitBridge.ProbeMp4Info(mp4Path);
#endif
}
}
catch (Exception e) { Debug.LogError("[vf-2] " + e); }
#endregion
// 转码完成,删除 .h264 临时文件
SafeDelete(h264Path);
// 失败时清理未完成 mp4
if (!convertOk || !File.Exists(mp4Path))
{
SafeDelete(mp4Path);
onComplete?.Invoke(false, "convert fail");
return false;
}
onComplete?.Invoke(true, mp4Path);
return true;
}
private static void SafeDelete(string p)
{
if (string.IsNullOrEmpty(p)) return;
try { if (File.Exists(p)) File.Delete(p); } catch { }
}
/// <summary>判断链接是否为 .h264/.264 裸流(mp4 等其他一律按可直接下载处理)</summary>
public static bool IsH264Url(string url)
{
if (string.IsNullOrEmpty(url)) return false;
var l = url.ToLowerInvariant();
return l.EndsWith(".h264") || l.EndsWith(".264", StringComparison.Ordinal);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e8c906cd03e5e1a439ced63198d73476
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

53
FFmpegKitFull/README.md Normal file
View File

@ -0,0 +1,53 @@
# FFmpegKit (full-gpl) 归档与放置说明
本项目用 ffmpeg-kit 在 Android 端做 H.264/裸流 → MP4 转码(libx264 重编码),
以修复各设备(如 OPPO/MTK)解码器对坏 PPS 流播放卡死的问题。
## 依赖来源
- Maven: `dev.ffmpegkit-maintained:ffmpeg-kit-full-gpl:6.0.3`
(原 `com.arthenica:ffmpeg-kit-full` 已于 2025-04 从 Maven Central 下架;`full` 不含 x264,必须用 `full-gpl`)
- FFmpeg 内部版本: **n6.1.6**
- 含 libx264(重编码用)
- 仅 arm64-v8a(项目只发布 arm64)
本目录 `ffmpeg-kit-full-gpl-6.0.3.aar` 即下载的原始包(36.6MB),作为来源/备份。
## 运行时文件位置(勿乱动!)
Unity Android 对 native 库与 jar 的打包位置有强约束,放错地方会导致 APK 缺少
native 库或类,运行时 `UnsatisfiedLinkError`/`ClassNotFoundException`。请**保持现状**:
- `.so``Assets/Plugins/Android/jniLibs/arm64-v8a/`
- libavcodec / libavdevice / libavfilter / libavformat / libavutil
- libswresample / libswscale
- libffmpegkit / libffmpegkit_abidetect
- **libc++_shared.so(必带,full 构建的 C++ 运行时依赖)**
- 类包 → `Assets/Plugins/Android/libs/ffmpeg-kit-classes.jar`
(由 `Assets/Plugins/Android/mainTemplate.gradle`
`implementation files('libs/ffmpeg-kit-classes.jar')` 引用)
## 调用链
- C# 桥接: `Assets/Scripts/Video/FFmpegKitBridge.cs`
- `#if UNITY_ANDROID && !UNITY_EDITOR`(Android 专用)
- JNI 必须在 Unity 主线程执行:经由 `MainThreadDispatcher.cs` 派发
- 转码参数: `-c:v libx264 -preset ultrafast -crf 23 -pix_fmt yuv420p -movflags +faststart -fflags +genpts`
- 视频页: `Assets/Scripts/UI/Pages/VideoPage/VideoPageCtrl.cs`
- 下载/播放服务: `Assets/Scripts/Video/VideoDownloadPlayService.cs`
## 升级方法
1. 重新下载目标版本 aar。
2. 解压,用 `jni/arm64-v8a/*.so` 整体覆盖上面 jniLibs 目录(含 libc++_shared.so)。
3. 用 aar 内 `classes.jar` 覆盖 libs/ 下的 jar。
4. clean 重建 release(不要增量)。
## 备注
- x264 为 GPL,与服务端已用的 libx264 转码策略一致,License 口径以项目自身为准。
- 播放验证关键日志:
- `[VideoDownloadPlayService]` -> `[FFmpegKitBridge] args`(确认含 `libx264`)
- `rc=` `isSuccess=True``[ProbeMp4Info]` mp4 正常尺寸
- `[vf-3]` VideoPrepared 的 frameCount / length 正常
- 播放过程不应再出现长时间卡帧。

Binary file not shown.

View File

@ -18,7 +18,7 @@
"depth": 0,
"source": "git",
"dependencies": {},
"hash": "ad2a81ec3068eafd48753df1825a8dd7201ce60c"
"hash": "c7923746fa5e95916bf1a4087eceadb72305d5b5"
},
"com.unity.ext.nunit": {
"version": "1.0.6",