video plugins
This commit is contained in:
@@ -0,0 +1,590 @@
|
||||
/*----------------------------------------------------------------
|
||||
// Copyright (C) 2025 Beijing All rights reserved.
|
||||
//
|
||||
// Author: huachangmiao
|
||||
// Create Date: 2025/04/11
|
||||
// Module Describe:
|
||||
//----------------------------------------------------------------*/
|
||||
|
||||
//******************************************************************
|
||||
// CPU软解码
|
||||
// 适用平台
|
||||
// Editor
|
||||
// Windows
|
||||
// MacOS
|
||||
// Android
|
||||
// iOS
|
||||
// WebGL
|
||||
//******************************************************************
|
||||
|
||||
// #define ENABLE_COPY_NATIVE_DATA //复制Native数据到托管内存
|
||||
|
||||
#if UNITY_WEBGL
|
||||
#else
|
||||
#define ENABLE_AV_MALLOC //使用AV_MALLOC分配内存
|
||||
#endif
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using LeviathanVideo.Abstractions;
|
||||
using LeviathanVideo.Bindings.Linked;
|
||||
using Unity.Collections;
|
||||
using Unity.Collections.LowLevel.Unsafe;
|
||||
using UnityEngine;
|
||||
using System.Runtime.InteropServices;
|
||||
using AOT;
|
||||
|
||||
internal unsafe class LeviathanSoftwareDecoder : ILeviathanDecoder
|
||||
{
|
||||
// 日志辅助方法
|
||||
private static void Log(string message)
|
||||
{
|
||||
LeviathanLogConfig.Log("SoftwareDecoder", message);
|
||||
}
|
||||
|
||||
private static void LogWarning(string message)
|
||||
{
|
||||
LeviathanLogConfig.LogWarning("SoftwareDecoder", message);
|
||||
}
|
||||
|
||||
private static void LogError(string message)
|
||||
{
|
||||
LeviathanLogConfig.LogError("SoftwareDecoder", message);
|
||||
}
|
||||
|
||||
// 事件实现
|
||||
public event Action OnFirstFrameDecoded;
|
||||
private AVFormatContext* _pFormatContext;
|
||||
private AVCodecContext* _pVideoContext;
|
||||
|
||||
private int _videoStreamIndex;
|
||||
private AVFrame* _videoFrame;
|
||||
private AVPacket* _packet;
|
||||
|
||||
private AVIOContext* _pAvioCtx;
|
||||
private byte* _avioCtxBuffer;
|
||||
private BufferData _bufferData;
|
||||
private GCHandle _bdHandle;
|
||||
|
||||
#if ENABLE_AV_MALLOC
|
||||
#else
|
||||
private GCHandle _hAvioBuffer;
|
||||
#endif
|
||||
#if ENABLE_COPY_NATIVE_DATA
|
||||
private IntPtr _unmanagedPtr;
|
||||
#endif
|
||||
|
||||
// 私有字段
|
||||
private long _frameCount;
|
||||
private double _frameInterval;
|
||||
private string _codecName = "";
|
||||
private long _duration;
|
||||
private long _decodedFrameIndex;
|
||||
private bool _firstFrameDecoded = false;
|
||||
|
||||
// 线程安全锁 - 保护 Seek 和 Decode 操作不并发执行
|
||||
private readonly object _codecLock = new object();
|
||||
|
||||
// 实现接口属性
|
||||
public long FrameCount => _frameCount;
|
||||
public double FrameInterval => _frameInterval;
|
||||
public string CodecName => _codecName;
|
||||
public long Duration => _duration;
|
||||
public long DecodedFrameIndex => _decodedFrameIndex;
|
||||
public AVFrame* VideoFrame => _videoFrame;
|
||||
|
||||
// 循环播放属性(软件解码器不需要特殊处理)
|
||||
public bool IsLooping { get; set; } = false;
|
||||
|
||||
// 快速解码模式(跳过环路滤波器)
|
||||
private bool _fastDecode = true;
|
||||
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct BufferData
|
||||
{
|
||||
public byte* ptr;
|
||||
public byte* originPtr;
|
||||
public ulong size;
|
||||
public ulong totalSize;
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(avio_alloc_context_read_packet))]
|
||||
private static int ReadPacket(void* opaque, byte* buf, int bufSize)
|
||||
{
|
||||
BufferData* bd = (BufferData*)opaque;
|
||||
bufSize = (int)Math.Min(bd->size, (ulong)bufSize);
|
||||
|
||||
if (bufSize <= 0)
|
||||
return leviathan.AVERROR_EOF;
|
||||
|
||||
// Buffer.MemoryCopy(bd->ptr, buf, bufSize, bufSize);
|
||||
UnsafeUtility.MemCpy(buf, bd->ptr, bufSize);
|
||||
bd->ptr += bufSize;
|
||||
bd->size -= (ulong)bufSize;
|
||||
|
||||
return bufSize;
|
||||
}
|
||||
|
||||
[MonoPInvokeCallback(typeof(avio_alloc_context_seek))]
|
||||
private static long SeekPacket(void* opaque, long offset, int whence)
|
||||
{
|
||||
BufferData* bd = (BufferData*)opaque;
|
||||
switch (whence)
|
||||
{
|
||||
case 0: //SEEK_SET
|
||||
if (offset < 0 || (ulong)offset > bd->totalSize)
|
||||
return leviathan.AVERROR_Enum(leviathan.EINVAL);
|
||||
bd->ptr = bd->originPtr + offset;
|
||||
bd->size = bd->totalSize - (ulong)offset;
|
||||
return offset;
|
||||
|
||||
case 1: //SEEK_CUR
|
||||
long newPos = bd->ptr - bd->originPtr + offset;
|
||||
if (newPos < 0 || (ulong)newPos > bd->totalSize)
|
||||
return leviathan.AVERROR_Enum(leviathan.EINVAL);
|
||||
bd->ptr += offset;
|
||||
bd->size = bd->totalSize - (ulong)newPos;
|
||||
return newPos;
|
||||
|
||||
case 2: //SEEK_END
|
||||
long endPos = (long)bd->totalSize + offset;
|
||||
if (endPos < 0 || endPos > (long)bd->totalSize)
|
||||
return leviathan.AVERROR_Enum(leviathan.EINVAL);
|
||||
bd->ptr = bd->originPtr + endPos;
|
||||
bd->size = bd->totalSize - (ulong)endPos;
|
||||
return endPos;
|
||||
|
||||
default:
|
||||
return leviathan.AVERROR_Enum(leviathan.EINVAL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public int Init(NativeArray<byte> fileData, int beginFrameIndex = 1, bool syncDecodeFirstFrame = true, bool fastDecode = true)
|
||||
{
|
||||
LinkedBindings.Initialize();
|
||||
_fastDecode = fastDecode;
|
||||
var ret = 0;
|
||||
var src = fileData.GetUnsafeReadOnlyPtr();
|
||||
|
||||
#if ENABLE_COPY_NATIVE_DATA
|
||||
_unmanagedPtr = Marshal.AllocHGlobal(fileData.Length);
|
||||
var dst = _unmanagedPtr.ToPointer();
|
||||
UnsafeUtility.MemCpy(dst, src, fileData.Length);
|
||||
src = dst;
|
||||
#endif
|
||||
|
||||
ulong bufferSize = (ulong)fileData.Length;
|
||||
|
||||
_bufferData = new BufferData
|
||||
{
|
||||
originPtr = (byte*)src, // 初始化原始指针
|
||||
ptr = (byte*)src,
|
||||
size = bufferSize,
|
||||
totalSize = bufferSize
|
||||
};
|
||||
_bdHandle = GCHandle.Alloc(_bufferData, GCHandleType.Pinned);
|
||||
|
||||
avio_alloc_context_read_packet readCallback = ReadPacket;
|
||||
avio_alloc_context_seek seekCallback = SeekPacket;
|
||||
|
||||
// 为AVIOContext分配缓冲区
|
||||
const int avioCtxBufferSize = 4096;
|
||||
#if ENABLE_AV_MALLOC
|
||||
_avioCtxBuffer = (byte*)leviathan.av_malloc(avioCtxBufferSize);
|
||||
|
||||
// 分配AVIOContext并将其与AVFormatContext关联
|
||||
_pAvioCtx = leviathan.avio_alloc_context(
|
||||
_avioCtxBuffer,
|
||||
avioCtxBufferSize,
|
||||
0, // 不可写
|
||||
(void*)_bdHandle.AddrOfPinnedObject(),
|
||||
readCallback,
|
||||
null, // 无写回调
|
||||
seekCallback
|
||||
);
|
||||
#else
|
||||
// wasm无法直接使用av_malloc
|
||||
int alignment = 64;
|
||||
byte[] avioBufferManaged = new byte[avioCtxBufferSize + alignment - 1];
|
||||
_hAvioBuffer = GCHandle.Alloc(avioBufferManaged, GCHandleType.Pinned);
|
||||
IntPtr avioBufferPtr = _hAvioBuffer.AddrOfPinnedObject();
|
||||
// 调整指针到对齐地址
|
||||
IntPtr alignedPtr = new IntPtr((avioBufferPtr.ToInt64() + alignment - 1) & ~(alignment - 1));
|
||||
|
||||
// 分配AVIOContext并将其与AVFormatContext关联
|
||||
_pAvioCtx = leviathan.avio_alloc_context(
|
||||
(byte*)alignedPtr,
|
||||
avioCtxBufferSize,
|
||||
0, // 不可写
|
||||
(void*)_bdHandle.AddrOfPinnedObject(),
|
||||
readCallback,
|
||||
null, // 无写回调
|
||||
seekCallback
|
||||
);
|
||||
#endif
|
||||
if (_pAvioCtx == null)
|
||||
{
|
||||
ret = leviathan.AVERROR_Enum(leviathan.ENOMEM);
|
||||
LogError("无法分配AVIO上下文");
|
||||
return ret;
|
||||
}
|
||||
|
||||
_pFormatContext = leviathan.avformat_alloc_context();
|
||||
_pFormatContext->pb = _pAvioCtx;
|
||||
var pFormatContext = _pFormatContext;
|
||||
ret = leviathan.avformat_open_input(&pFormatContext, null, null, null);
|
||||
if (ret != 0)
|
||||
{
|
||||
LogError($"avformat_open_input failed: {ret}");
|
||||
return ret;
|
||||
}
|
||||
if (_pFormatContext == null)
|
||||
{
|
||||
return ret;
|
||||
}
|
||||
|
||||
return InternalPlay(beginFrameIndex, syncDecodeFirstFrame);
|
||||
}
|
||||
|
||||
public int InternalPlay(int beginFrameIndex = 1, bool syncDecodeFirstFrame = true)
|
||||
{
|
||||
leviathan.avformat_find_stream_info(_pFormatContext, null);
|
||||
AVCodec* videoCodec = null;
|
||||
_videoStreamIndex = leviathan.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, &videoCodec, 0);
|
||||
// 获取视频流信息
|
||||
AVStream* videoStream = _pFormatContext->streams[_videoStreamIndex];
|
||||
// 计算帧间隔
|
||||
AVRational frameRate = videoStream->r_frame_rate;
|
||||
double fps = leviathan.av_q2d(frameRate);
|
||||
_frameInterval = 1.0 / fps;
|
||||
|
||||
_pVideoContext = leviathan.avcodec_alloc_context3(videoCodec);
|
||||
|
||||
#if UNITY_EDITOR
|
||||
_codecName = leviathan.avcodec_get_name(videoCodec->id);
|
||||
#endif
|
||||
if (videoCodec == null)
|
||||
{
|
||||
LogError("videoCodec is null");
|
||||
return 1;
|
||||
}
|
||||
_pVideoContext->thread_count = 0;
|
||||
_pVideoContext->thread_type = leviathan.FF_THREAD_FRAME | leviathan.FF_THREAD_SLICE;
|
||||
|
||||
// 根据 fastDecode 参数决定是否跳过环路滤波器以提升解码速度
|
||||
if (_fastDecode)
|
||||
{
|
||||
_pVideoContext->skip_loop_filter = (AVDiscard)48; //AVDISCARD_ALL
|
||||
}
|
||||
|
||||
// Debug.Log($"{_pVideoContext->codec_id} {_frameInterval} {_pFormatContext->duration}");
|
||||
leviathan.avcodec_open2(_pVideoContext, videoCodec, null);
|
||||
|
||||
_duration = _pFormatContext->duration;
|
||||
|
||||
if (_pFormatContext->streams[_videoStreamIndex]->nb_frames > 0)
|
||||
{
|
||||
_frameCount = _pFormatContext->streams[_videoStreamIndex]->nb_frames;
|
||||
}
|
||||
else
|
||||
{
|
||||
var dur = _duration / 1000.0 * leviathan.av_q2d(videoStream->time_base);
|
||||
int frNum = frameRate.num;
|
||||
int frDen = frameRate.den;
|
||||
if (dur > 0 && frNum > 0 && frDen > 0)
|
||||
{
|
||||
_frameCount = (long)(dur * frNum / frDen);
|
||||
}
|
||||
}
|
||||
// Debug.Log($"frameCount: {_frameCount}");
|
||||
|
||||
_videoFrame = leviathan.av_frame_alloc();
|
||||
_packet = leviathan.av_packet_alloc();
|
||||
|
||||
if (beginFrameIndex > 1)
|
||||
{
|
||||
SeekToFrame(beginFrameIndex);
|
||||
}
|
||||
|
||||
if (syncDecodeFirstFrame)
|
||||
{
|
||||
// 第一帧同步解码
|
||||
int ret = DecodeNextFrame();
|
||||
if (ret != 0)
|
||||
{
|
||||
LogError($"DecodeNextFrame ret: {ret}");
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
public int DecodeNextFrame()
|
||||
{
|
||||
lock (_codecLock)
|
||||
{
|
||||
if (_pVideoContext == null || _pFormatContext == null)
|
||||
return -1;
|
||||
|
||||
return DecodeNextFrameInternal();
|
||||
}
|
||||
}
|
||||
|
||||
private int DecodeNextFrameInternal()
|
||||
{
|
||||
var flushSent = false;
|
||||
while (true)
|
||||
{
|
||||
var error = leviathan.avcodec_receive_frame(_pVideoContext, _videoFrame);
|
||||
if (error == 0)
|
||||
{
|
||||
long frameIndex = 1;
|
||||
if (_frameInterval > 0)
|
||||
{
|
||||
var timeBase = _pFormatContext->streams[_videoStreamIndex]->time_base;
|
||||
double frameTime = _videoFrame->pts * leviathan.av_q2d(timeBase);
|
||||
frameIndex = (long)(frameTime / _frameInterval + 0.1) + 1;
|
||||
}
|
||||
Interlocked.Exchange(ref _decodedFrameIndex, frameIndex);
|
||||
|
||||
// 触发第一帧回调
|
||||
if (!_firstFrameDecoded)
|
||||
{
|
||||
_firstFrameDecoded = true;
|
||||
OnFirstFrameDecoded?.Invoke();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (error == leviathan.AVERROR_Enum(leviathan.EAGAIN))
|
||||
{
|
||||
if (flushSent)
|
||||
{
|
||||
// 已发送过flush,无更多数据
|
||||
return 1;
|
||||
}
|
||||
|
||||
error = leviathan.av_read_frame(_pFormatContext, _packet);
|
||||
if (error < 0)
|
||||
{
|
||||
if (error == leviathan.AVERROR_EOF)
|
||||
{
|
||||
// 发送flush packet以取出解码器中剩余帧
|
||||
error = leviathan.avcodec_send_packet(_pVideoContext, null);
|
||||
flushSent = true;
|
||||
if (error < 0 && error != leviathan.AVERROR_Enum(leviathan.EAGAIN))
|
||||
{
|
||||
return 2;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 其他错误,继续尝试读取
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (_packet->stream_index == _videoStreamIndex)
|
||||
{
|
||||
error = leviathan.avcodec_send_packet(_pVideoContext, _packet);
|
||||
if (error < 0)
|
||||
{
|
||||
leviathan.av_packet_unref(_packet);
|
||||
if (error == leviathan.AVERROR_Enum(leviathan.EAGAIN))
|
||||
{
|
||||
// 解码器需要先输出帧,继续循环处理
|
||||
continue;
|
||||
}
|
||||
else
|
||||
{
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
leviathan.av_packet_unref(_packet);
|
||||
}
|
||||
else if (error == leviathan.AVERROR_EOF)
|
||||
{
|
||||
// 解码器已无更多帧
|
||||
return 4;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 其他错误
|
||||
return error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void InternalStop()
|
||||
{
|
||||
lock (_codecLock)
|
||||
{
|
||||
// Release resources
|
||||
var pVideoContext = _pVideoContext;
|
||||
var pFormatContext = _pFormatContext;
|
||||
var videoFrame = _videoFrame;
|
||||
var packet = _packet;
|
||||
if (_pVideoContext != null) leviathan.avcodec_free_context(&pVideoContext);
|
||||
if (_pFormatContext != null) leviathan.avformat_close_input(&pFormatContext);
|
||||
if (_videoFrame != null) leviathan.av_frame_free(&videoFrame);
|
||||
if (_packet != null) leviathan.av_packet_free(&packet);
|
||||
|
||||
_pVideoContext = null;
|
||||
_pFormatContext = null;
|
||||
_videoFrame = null;
|
||||
_packet = null;
|
||||
}
|
||||
|
||||
if (_pAvioCtx != null)
|
||||
{
|
||||
#if ENABLE_AV_MALLOC
|
||||
leviathan.av_free(_pAvioCtx->buffer);
|
||||
#else
|
||||
_pAvioCtx->buffer = null; // 关键:阻止 FFmpeg 调用 av_free
|
||||
if (_hAvioBuffer.IsAllocated)
|
||||
{
|
||||
_hAvioBuffer.Free();
|
||||
}
|
||||
#endif
|
||||
var pAvioCtx = _pAvioCtx;
|
||||
leviathan.avio_context_free(&pAvioCtx);
|
||||
_pAvioCtx = null;
|
||||
}
|
||||
|
||||
if (_bdHandle.IsAllocated)
|
||||
{
|
||||
_bdHandle.Free();
|
||||
}
|
||||
_bdHandle = default;
|
||||
#if ENABLE_COPY_NATIVE_DATA
|
||||
if (_unmanagedPtr != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(_unmanagedPtr);
|
||||
}
|
||||
_unmanagedPtr = default;
|
||||
#endif
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
InternalStop();
|
||||
}
|
||||
|
||||
// 跳转到指定时间点
|
||||
public void Seek(double ms)
|
||||
{
|
||||
lock (_codecLock)
|
||||
{
|
||||
if (_pVideoContext == null || _pFormatContext == null)
|
||||
return;
|
||||
|
||||
long targetFrame = (long)(_frameInterval * ms);
|
||||
// Debug.Log("Seek to " + targetFrame + "(" + ms + "ms)");
|
||||
leviathan.avformat_seek_file(_pFormatContext, _videoStreamIndex, Int64.MinValue, targetFrame, Int64.MaxValue, leviathan.AVSEEK_FLAG_FRAME);
|
||||
leviathan.avcodec_flush_buffers(_pVideoContext);
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到指定帧
|
||||
public void SeekToFrame(long frame)
|
||||
{
|
||||
lock (_codecLock)
|
||||
{
|
||||
if (_pVideoContext == null || _pFormatContext == null)
|
||||
return;
|
||||
|
||||
long pts = _pFormatContext->streams[_videoStreamIndex]->duration / _frameCount * frame - 1;
|
||||
leviathan.avformat_seek_file(_pFormatContext, _videoStreamIndex, Int64.MinValue, pts, Int64.MaxValue, leviathan.AVSEEK_FLAG_FRAME);
|
||||
leviathan.avcodec_flush_buffers(_pVideoContext);
|
||||
}
|
||||
}
|
||||
|
||||
// 获取decodedFrameIndex的引用,用于Interlocked操作
|
||||
public ref long GetDecodedFrameIndexRef()
|
||||
{
|
||||
return ref _decodedFrameIndex;
|
||||
}
|
||||
|
||||
// 暂停/恢复播放的空方法(软件解码器不需要特殊处理)
|
||||
public void Pause(bool pause)
|
||||
{
|
||||
// 软件解码器的暂停由LeviathanVideoDecoder的_isPause标志控制
|
||||
// 这里不需要额外的实现
|
||||
}
|
||||
|
||||
// 微信平台特有方法的空实现
|
||||
public int GetDecoderStatus()
|
||||
{
|
||||
// 软件解码器始终返回已启动状态
|
||||
return 2;
|
||||
}
|
||||
|
||||
// 将视频帧数据复制到纹理(需要在信号保护下执行,访问_videoFrame)
|
||||
public void CopyFrameDataToTextures(UnityEngine.Texture2D[] textures)
|
||||
{
|
||||
const int TextureCount = 3;
|
||||
|
||||
if (textures == null || textures.Length < TextureCount) return;
|
||||
|
||||
// 更新YUV纹理 - 只做SetPixelData,不做Apply
|
||||
for (uint i = 0; i < TextureCount; i++)
|
||||
{
|
||||
if (_videoFrame->data[i] == null || textures[i] == null) continue;
|
||||
|
||||
var nativeArray = Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.ConvertExistingDataToNativeArray<byte>(
|
||||
_videoFrame->data[i],
|
||||
textures[i].width * textures[i].height,
|
||||
Unity.Collections.Allocator.None
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.SetAtomicSafetyHandle(ref nativeArray, Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.Create());
|
||||
#endif
|
||||
// 只复制数据到纹理的CPU缓冲区,不上传到GPU
|
||||
textures[i].SetPixelData(nativeArray, 0);
|
||||
#if ENABLE_UNITY_COLLECTIONS_CHECKS
|
||||
Unity.Collections.LowLevel.Unsafe.AtomicSafetyHandle.Release(Unity.Collections.LowLevel.Unsafe.NativeArrayUnsafeUtility.GetAtomicSafetyHandle(nativeArray));
|
||||
#endif
|
||||
}
|
||||
catch (UnityException)
|
||||
{
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
nativeArray.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将纹理数据应用到GPU(可以和下一帧解码并行执行)
|
||||
public void ApplyTextures(UnityEngine.Texture2D[] textures)
|
||||
{
|
||||
const int TextureCount = 3;
|
||||
|
||||
if (textures == null || textures.Length < TextureCount) return;
|
||||
|
||||
// 将CPU数据上传到GPU - 不需要访问_videoFrame
|
||||
for (uint i = 0; i < TextureCount; i++)
|
||||
{
|
||||
if (textures[i] == null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
textures[i].Apply(false, false);
|
||||
}
|
||||
catch (UnityException)
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 8483db06286d50244a0f52a17aa423e0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 373be47a4cbf74440b38b8262a3b7ded
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user