본문 바로가기
C#

[C#] mp4 Header 읽기를 통해 형식 및 duration 구하기

by Jcoder 2021. 11. 2.

1. ftyp 읽기

2. mdat 건너띄기 (mdat는 영상 데이터 부분)

3. moov의 mvhd duration 구하기

 

참고 : https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10.pdf

참고 : https://videocube.tistory.com/entry/MP4-%EB%B6%84%EC%84%9D-%ED%95%98%EA%B8%B0-MPEG4-%ED%8C%8C%ED%8A%B8-14

 

[MP4] 분석 하기 | MPEG-4 파트 14 | MP4Box 설치

Wiki 에 따르면 MP4 는 공식적으로 ISO/IEC 14496-14:2003 MPEG-4의 일부 규정된 멀티미디어 컨테이너 포맷이다. ㄴ 디지털 비디오, 디지털 오디오 스트림을 저장 하기 위해서 사용함 ㄴ 자막, 스틸 이미지

videocube.tistory.com

 

인터넷에서는 ffprobe를 통해 읽는 것이 많으나 mp4 metadata를 통해 읽어왔다.

결과

 

ffmpeg -f flv rtmp를 통해 송출을 하면 형식은  [결과] 사진에 있는 ed97dad20ef370904c790102d2fec112949ce8e987f10817ab7773c7ffc9b669.mp4 처럼 f4v가 된다.

만약 송출 시간이 길다면 용량은 많이 늘어나게 되어서  isom 형식으로 변경하였다.

 

using System;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.IO;
using System.Text;
using System.Runtime.InteropServices.ComTypes;

namespace VideoEncoding_console
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();
            var test1 = await GetMetaDataAsync(@"C:\Users\tjdsk\Desktop\ed97dad20ef370904c790102d2fec112949ce8e987f10817ab7773c7ffc9b669.mp4");
            stopwatch.Stop();
            Console.WriteLine(test1);
            Console.WriteLine(stopwatch.ElapsedMilliseconds + Environment.NewLine);

            var test2 = await GetMetaDataAsync(@"C:\Users\tjdsk\Desktop\tested97dad20ef370904c790102d2fec112949ce8e987f10817ab7773c7ffc9b669.mp4");
            Console.WriteLine(test2 + Environment.NewLine);

            Console.ReadLine();
        }

        static async Task<MetaData> GetMetaDataAsync(string fileFullPath)
        {
            // https://www.adobe.com/content/dam/acom/en/devnet/flv/video_file_format_spec_v10.pdf
            // https://videocube.tistory.com/entry/MP4-%EB%B6%84%EC%84%9D-%ED%95%98%EA%B8%B0-MPEG4-%ED%8C%8C%ED%8A%B8-14
             
            if (!System.IO.File.Exists(fileFullPath))
                return null;
            
            MetaData result = new MetaData();

            using FileStream fileStream = new FileStream(fileFullPath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite);

            if (fileStream.Length >= 4)
            {
                byte[] ftypbytesSize = new byte[4];
                await fileStream.ReadAsync(ftypbytesSize.AsMemory(0, 4)); // ftyp
                if (BitConverter.IsLittleEndian)
                    Array.Reverse(ftypbytesSize);
                
                int ftypBoxSize = BitConverter.ToInt32(ftypbytesSize, 0); // ftyp 데이터 사이즈
                if (ftypBoxSize > 0)
                {
                    byte[] ftypbytes = new byte[ftypBoxSize];
                    int ftypReadbytes = await fileStream.ReadAsync(ftypbytes.AsMemory(0, ftypBoxSize)); // ftyp 데이터 읽기

                    string fileFormat = string.Empty;
                    string compatibleBrands = string.Empty;
                    string duration = string.Empty;

                    if (ftypReadbytes > 0)
                    {
                        fileFormat = Encoding.UTF8.GetString(ftypbytes, 4, 4).Trim(); // f4v 인지 isom 인지
                        compatibleBrands = Encoding.UTF8.GetString(ftypbytes, 12, 16).Replace("\0", "").Replace("\b", "").Trim(); // isommp42m4v 인지 isomiso2avc1mp41

                        byte[] mdatbytesSize = new byte[4];
                        fileStream.Position = fileStream.Position + 4; // mdat 파일 사이즈 부분으로 이동.
                        await fileStream.ReadAsync(mdatbytesSize.AsMemory(0, 4)); // mdat 사이즈 읽기
                        if (BitConverter.IsLittleEndian)
                            Array.Reverse(mdatbytesSize);

                        int mdatBoxSize = BitConverter.ToInt32(mdatbytesSize, 0);
                        if (mdatBoxSize > 0)
                        {
                            fileStream.Position = fileStream.Position + mdatBoxSize + 4 + 4 + 4; // mdat 데이터 부분 + moov + mvhd 사이즈 + mvhd

                            byte[] mvhdVersionbytes = new byte[1];
                            await fileStream.ReadAsync(mvhdVersionbytes.AsMemory(0, 1)); // Movie Header Atom 의 버전, 1: 64bits 0: 32: bits(시간)

                            if (mvhdVersionbytes[0] == 0) // 32비트 시간
                            {
                                fileStream.Position = fileStream.Position + 3 + 4 + 4; // Flags + Creation-time + Modification-tim

                                byte[] timescalebytes = new byte[4];
                                await fileStream.ReadAsync(timescalebytes.AsMemory(0, 4)); // timescale

                                byte[] durationbytes = new byte[4];
                                await fileStream.ReadAsync(durationbytes.AsMemory(0, 4)); // duration

                                if (BitConverter.IsLittleEndian)
                                {
                                    Array.Reverse(timescalebytes);
                                    Array.Reverse(durationbytes);
                                }

                                int timeScaleValue = BitConverter.ToInt32(timescalebytes, 0);
                                int durationValue = BitConverter.ToInt32(durationbytes, 0);
                                duration = $"{durationValue / timeScaleValue}";
                            }
                            else if (mvhdVersionbytes[0] == 1) // 64비트 시간
                            {
                                fileStream.Position = fileStream.Position + 3 + 8 + 8; // Flags + Creation-time + Modification-tim

                                byte[] timescalebytes = new byte[4];
                                await fileStream.ReadAsync(timescalebytes.AsMemory(0, 4)); // timescale

                                byte[] durationbytes = new byte[8];
                                await fileStream.ReadAsync(durationbytes.AsMemory(0, 8)); // duration

                                if (BitConverter.IsLittleEndian)
                                {
                                    Array.Reverse(timescalebytes);
                                    Array.Reverse(durationbytes);
                                }

                                int timeScaleValue = BitConverter.ToInt32(timescalebytes, 0);
                                long durationValue = BitConverter.ToInt64(durationbytes, 0);
                                duration = $"{durationValue / timeScaleValue}";
                            }
                            result.Filename = fileStream.Name;
                            result.FileSize = fileStream.Length.ToString();
                            result.Duration = duration;
                            result.FileFormat = fileFormat;
                            result.CompatibleBrands = compatibleBrands;
                        }
                    }
                }
            }

            return string.IsNullOrEmpty(result.Filename) ? null : result;
        }
    }

    public class MetaData
    {
        public string Filename { get; set; }
        public string Duration { get; set; }
        public string FileSize { get; set; }
        public string FileFormat { get; set; }
        public string CompatibleBrands { get; set; }
        public override string ToString() => $"Filename : {Filename}{Environment.NewLine}Duration: {Duration}{Environment.NewLine}Filesize: {FileSize}{Environment.NewLine}FileFormat: {FileFormat}{Environment.NewLine}CompatibleBrands: {CompatibleBrands}";
    }
}