Unity VideoPlayer로 한 프레임씩 동영상 불러오기

Unity에서 동영상을 불러오려면 보통 VideoPlayer를 쓰거나 OpenCV의 VideoCapture를 많이 쓰게 된다. 여기서는 https://forum.unity.com/threads/how-to-extract-frames-from-a-video.853687/ 여기 링크를 참고하여 VideoPlayer를 사용하는 방법을 설명한다.


VideoPlayer는 unity 컴포넌트라서 new VideoPlayer() 식으로 할당하면 안 되고 씬에서 생성한 다음에 넣어줘야 하는데 스크립트에서만 동작시키고 싶으면 Start()와 같은 곳에서 다음과 같이 쓰면 된다.


VideoPlayer videoPlayer = gameObject.AddComponent(typeof(VideoPlayer)) as VideoPlayer;
videoPlayer.Stop();
videoPlayer.renderMode = VideoRenderMode.APIOnly;
videoPlayer.prepareCompleted += Prepared;
videoPlayer.sendFrameReadyEvents = true;
videoPlayer.frameReady += FrameReady;


gameObject가 필요하니까 MonoBehavior 클래스를 상속받아야 한다. 어차피 유니티 모듈이니까 아예 유니티와 상관없이 동작은 안 되는 것이다. 위의 코드는 그냥 예제라서 로컬로 선언했지만 멤버로 선언하는게 맞으르 것이다.

받아오자마자 Stop()부터 하고 APIOnly 모드로 설정한다. 이는 화면에 직접 플레이하는 일은 안 하고 내부적으로만 동작하겠다는 것이다.

preparedCompleted 는 동영상의 헤더를 다 읽어와서 준비가 되면 호출된다. frameReady는 프레임 한 장을 grab 하는 일이 완료되면 불러와진다. 매 프레임마다 불러와지는 것이다.


이제 아래와 같이 주소값을 넣어주고 Prepare를 호출한다.


videoPlayer.url = path;
videoPlayer.Prepare();


여기서 path를 잘 넣어줘야 한다. 아래와 같은 구문을 참고해보자.


string outputPath = System.IO.Path.Combine(Application.streamingAssetsPath, "result.avi");


준비가 완료되었을 때 호출될 함수는 다음과 같다.


void Prepared(VideoPlayer vp)
{        
    vp.Pause();
}


이렇게 포즈를 걸면 0번째 프레임을 불러온다.


void FrameReady(VideoPlayer vp, long frameIndex)
{
    var textureToCopy = vp.texture;

    //initialize
    if (frameIndex == 0)
    {
        images = new List<Mat>();
        texture = new Texture2D((int)vp.width, (int)vp.height, TextureFormat.RGB24, false);
        Debug.Log(vp.width + "" + vp.height);
        Debug.Log(textureToCopy.width + "" + textureToCopy.height);
    }

    if (saveImage)
    {
        Mat mat = new Mat((int)vp.height, (int)vp.width, OpenCVForUnity.CoreModule.CvType.CV_8UC3);
        OpenCVForUnity.UnityUtils.Utils.textureToTexture2D(textureToCopy, texture);
        OpenCVForUnity.UnityUtils.Utils.texture2DToMat(texture, mat);
        images.Add(mat);
    }        

    processRate = frameIndex / (double)videoPlayer.frameCount;
    ProcessRateChanged.Invoke();
    if (frameIndex + 1 == (long)vp.frameCount)
    {
        Debug.Log("INVOKE!!");
        ReadCompleted.Invoke();
    }

    if (frameIndex < frameLimit || (ulong)frameIndex < vp.frameCount - 1 )
    {
        vp.frame = frameIndex + 1;
    }
    else
    {
        ReadCompleted.Invoke();
    }
}


중간에 frameIndex == 0 블럭이랑 saveImage 블럭은 내가 OpenCVForUnity를 활용하기 위해 작성한 것이니 신경쓰지 말자. 하여튼 중요한 것은 textureToCopy가 들어왔다는 것이고, 이것을 이용하여 본인 원하는 작업을 하면 된다.

맨 마지막에 frameLimit는 내가 직접 선언한 것이니 신경쓰지 말고, 중요한 것은 vp.frame에 그 다음으로 읽고 싶은 프레임 번호를 넣는 것이다. vp.frame은 단순한 멤버 변수가 아니고 property이기 때문에 값을 집어넣는 순간 다음 프레임을 읽어오는 동작을 수행한다.

ReadComplete도 내가 직접 만든 것이다. 하여튼 여기에서 다 읽어왔을 때의 동작을 수행하면 된다.


이 방법의 문제점은 Prepare에 실패했을 때 아무런 피드백이 없다는 것이다. Prepare()를 호출하고 얼마간 지났는데도 Prepared 이벤트 함수로 들어오지 않는다면 뭔가 조치를 취할 수 있도록 해야 한다. 우선 아래와 같은 무식한 방법이 있긴 하다.


int count = 0;
while (!videoPlayer.isPrepared && count++ < 100)
{
    Debug.Log("Preparing Video");
    yield return null;
}
Debug.Log("Done Preparing Video");


100 frame이니까 약 3초 정도 기다려보는 것이다. while을 빠져나오고 나면 isPrepared를 다시 검사해서 준비가 되서 빠져나온 건지 아니면 count가 다 되서 빠져나온 건지 확인해보면 된다.

또한 일부 Unity 플러그인에서 레코딩한 파일은 몇 프레임 읽다가 실패한다. 상당수의 비디오 레코더들이 표준 포맷을 지키지 않고 파일을 쓴다. 여타 다른 비디오 플레이어들은 어떻게든 잘못된 포맷 형태를 무시하고 동영상을 플레이해주는데, VideoPlayer는 그들만큼 정성스럽지가 못하다.

유니티에서 공식적으로 지원한다고 밝힌 확장자는 아래와 같다.

https://docs.unity3d.com/Manual/VideoSources-FileCompatibility.html

그런데 여기에 함정이 있다. mp4의 경우, h.264는 잘 불러와지는데 h.265는 따로 코덱을 깔아야 한다. 나는 대충 깔아서 해봤는데 잘 안 되고 신통치 않다;; 같은 avi, mp4 확장자라도 코덱은 천차만별이라 신경 쓰이는 게 한두가지가 아니다. 게다가 윈도우가 아닌 안드로이드로 넘어가면 골치아픔은 두 배가 된다.


0 comments:

댓글 쓰기

Powered by Blogger.