解码专有的H264网络视频流。

3

我在我的Spring Boot应用程序中通过WebSocket从RTSP相机接收到可能编码为H264的字节流,

我需要解码传入的H264流以将视频传输到我的前端客户端。 我尝试使用javaCV / FFMpeg,但没有任何作用。

非常感谢您的帮助

这是通过套接字接收到的十六进制转储的一部分

00000000: 01 00 00 00 04 48 32 36 34 00 00 00 24 38 65 34    .....H264...$8e4
00000010: 32 39 65 37 61 2D 32 66 34 66 2D 34 37 31 61 2D    29e7a-2f4f-471a-
00000020: 39 61 63 30 2D 66 66 62 38 64 64 37 63 37 64 37    9ac0-ffb8dd7c7d7
00000030: 32 00 00 00 D4 7B 22 49 73 49 6E 69 74 22 3A 66    2...T{"IsInit":f
00000040: 61 6C 73 65 2C 22 49 73 41 75 64 69 6F 22 3A 66    alse,"IsAudio":f
00000050: 61 6C 73 65 2C 22 54 6F 74 61 6C 53 65 63 6F 6E    alse,"TotalSecon
00000060: 64 73 22 3A 30 2E 30 36 2C 22 46 72 61 6D 65 54    ds":0.06,"FrameT
00000070: 69 6D 65 22 3A 22 32 30 32 33 2D 30 32 2D 32 33    ime":"2023-02-23
00000080: 54 30 34 3A 32 31 3A 35 33 2E 35 33 31 5A 22 2C    T04:21:53.531Z",
00000090: 22 53 65 71 75 65 6E 63 65 49 64 22 3A 31 2C 22    "SequenceId":1,"
000000a0: 42 61 73 65 44 65 63 6F 64 65 54 69 6D 65 22 3A    BaseDecodeTime":
000000b0: 32 36 35 38 37 2C 22 4D 65 64 69 61 54 69 6D 65    26587,"MediaTime
000000c0: 22 3A 32 36 35 38 37 2C 22 49 73 46 72 61 6D 65    ":26587,"IsFrame
000000d0: 48 69 64 64 65 6E 22 3A 66 61 6C 73 65 2C 22 49    Hidden":false,"I
000000e0: 73 4B 65 79 46 72 61 6D 65 22 3A 66 61 6C 73 65    sKeyFrame":false
000000f0: 2C 22 49 64 22 3A 34 34 35 2C 22 47 65 6E 65 72    ,"Id":445,"Gener
00000100: 61 74 69 6F 6E 22 3A 31 7D 00 00 3F 50 00 00 00    ation":1}..?P...
00000110: 68 6D 6F 6F 66 00 00 00 10 6D 66 68 64 00 00 00    hmoof....mfhd...
00000120: 00 00 00 01 BD 00 00 00 50 74 72 61 66 00 00 00    ....=...Ptraf...
00000130: 10 74 66 68 64 00 02 00 00 00 00 00 01 00 00 00    .tfhd...........
00000140: 14 74 66 64 74 01 00 00 00 00 00 00 00 00 00 67    .tfdt..........g
00000150: DB 00 00 00 24 74 72 75 6E 01 00 0F 01 00 00 00    [...$trun.......
00000160: 01 00 00 00 70 00 00 00 3C 00 00 3E E0 00 01 00    ....p...<..>`...
00000170: 00 00 00 00 00 00 00 3E E8 6D 64 61 74 00 00 3E    .......>hmdat..>
00000180: DC 41 E1 81 80 93 BE 16 2B 33 77 3D 4C B6 55 8B    \Aa...>.+3w=L6U.
00000190: D2 55 60 92 05 F7 F7 A4 97 54 4B 6C A6 68 48 84    RU`..ww$.TKl&hH.
000001a0: 68 FF D2 B6 6C 02 31 FC 24 01 78 EA BD 20 AD 15    h.R6l.1|$.xj=.-.
000001b0: F1 73 31 4B EB EF 18 1B 50 B3 13 F2 DC C6 4C E1    qs1Kko..P3.r\FLa
000001c0: 75 8B 94 52 6B C5 09 37 55 1E 45 66 6A 92 39 23    u..RkE.7U.Efj.9#
000001d0: C9 2D FD BB EC AD FD CF C4 30 75 FF 44 66 FA 85    I-};l-}OD0u.Dfz.
000001e0: D9 7C 18 72 AE 63 45 60 DD D7 65 44 84 49 95 8D    Y|.r.cE`]WeD.I..
000001f0: 2C 70 6C 57 8E E9 A9 EB B6 F6 78 BD D6 88 99 F6    ,plW.i)k6vx=V..v
00000200: FC 25 B1 0A FF DF CB 77 6A 67 37 24 A5 3D 8F A1    |%1.._Kwjg7$%=.!
00000210: 27 9B 4F 42 0E CD B8 87 6E C9 99 FC 6F 4C 53 4B    '.OB.M8.nI.|oLSK
00000220: 01 EA B6 AF 99 F8 22 C1 8F 1E C1 66 D6 8A 09 D6    .j6/.x"A..AfV..V
00000230: 99 79 91 F7 C1 2A 08 1F 81 CB 5E DD C3 CA 86 8F    .y.wA*...K^]CJ..
00000240: 57 BF 17 A2 64 6B 69 56 AE 19 1F 57 AD A6 D8 C2    W?."dkiV...W-&XB
00000250: 06 28 EB 46 D3 E4 85 51 3E E2 A5 40 50 50 85 7D    .(kFSd.Q>b%@PP.}
00000260: 72 6B 20 87 1A 6E 73 E1 B8 88 9E 20 23 48 6D FE    rk...nsa8...#Hm~
00000270: C2 0D 39 ED 24 B2 6D B5 9B 81 B6 BC F4 EE DE A2    B.9m$2m5..6<tn^"
00000280: CF A1 08 D0 D2 5B EE FA 0D DA FD 3B 79 C7 89 E5    O!.PR[nz.Z};yG.e
00000290: 4F 64 73 37 98 D6 2D 47 1D 8B A3 47 DD EA C9 8E    Ods7.V-G..#G]jI.
000002a0: 3E 8C 97 E2 42 15 FB 22 A6 83 A1 34 18 52 5E 35    >..bB.{"&.!4.R^5
000002b0: 2A A6 E2 71 D7 4F 96 0A EC AE 8D 39 27 B8 CF 61    *&bqWO..l..9'8Oa
000002c0: CC ED E9 AF 74 C3 95 D3 E3 96 32 20 E6 31 0B E4    Lmi/tC.Sc.2.f1.d
000002d0: DC F4 FF 41 37 36 E7 DB 87 AE B3 7D BF CA F8 05    \t.A76g[..3}?Jx.
000002e0: 72 2A 38 AB B8 8E 98 43 97 C8 5E 80 57 C6 E7 1E    r*8+8..C.H^.WFg.
000002f0: 86 75 CE CD CE BF CF 10 C9 8A C2 C9 6E 33 41 AC    .uNMN?O.I.BIn3A,
00000300: 91 AC A8 F3 1B E6 D5 0A 22 A1 2C 4C 68 19 51 4D    .,(s.fU."!,Lh.QM
00000310: 17 DA AE E1 D7 BC 0E 2D F8 14 61 E2 4F BA 26 A3    .Z.aW<.-x.abO:&#
00000320: 0A E4 A6 BE 08 EA 3C 28 E6 C5 6B CA 3A 86 D2 59    .d&>.j<(fEkJ:.RY
00000330: 34 C2 ED 91 72 5A EF 2C BE D7 38 A4 60 D7 F3 97    4Bm.rZo,>W8$`Ws.
00000340: BB E6 FD C2 D0 29 10 B5 A4 79 D8 3E 61 48 8A F9    ;f}BP).5$yX>aH.y
00000350: C6 D8 13 D0 FD DB D6 FA 24 7F CD 5A BF 06 57 49    FX.P}[Vz$.MZ?.WI
00000360: 51 EC ED B2 74 AB 92 1D 37 68 70 A2 A5 31 B5 5F    Qlm2t+..7hp"%15_
00000370: EA CF 9E 3E 6A B1 78 16 B7 94 D1 46 7B 63 C1 67    jO.>j1x.7.QF{cAg
00000380: D2 B0 08 44 64 1E 68 15 39 80 E3 DD EB C0 E1 71    R0.Dd.h.9.c]k@aq
00000390: E8 EE D0 4D DF 4F 41 E0 96 C5 34 AD BC D3 9E 88    hnPM_OA`.E4-<S..
000003a0: 0B 17 D8 7D 3A A8 3B 06 78 79 93 B7 30 92 C8 D8    ..X}:(;.xy.70.HX
000003b0: 5D 27 04 D7 00 9F E3 EA A3 C6 BD B9 05 21 5C 68    ]'.W..cj#F=9.!\h
000003c0: 45 DB 90 2A 05 38 79 D9 84 60 C7 F2 BB DE 1B 5A    E[.*.8yY.`Gr;^.Z
000003d0: 44 0B ED 67 34 DF 07 8B F5 04 27 9E 1A F0 04 CA    D.mg4_..u.'..p.J
000003e0: 86 B1 2C 0B 78 D0 58 86 81 62 D8 70 3D BA 9D 51    .1,.xPX..bXp=:.Q
000003f0: D8 2C 6C 6A 10 88 B9 F8 89 3D 6F 39 C2 52 49 CF    X,lj..9x.=o9BRIO
00000400: 9F C1 50 6A D4 9E A5 96 B2 0A 99 1D 6B BC 63 03    .APjT.%.2...k<c.
00000410: A4 8C 7E 1D BD DF 8B D8 97 EE 9A 59 78 63 FC 74    $.~.=_.X.n.Yxc|t
00000420: 3B 40 75 AF A7 1A B7 F0 56 A5 5F 3E 81 54 83 A0    ;@u/'.7pV%_>.T..
00000430: 7F FC AD 71 CE AF 54 8B 5D DC 27 34 20 A3 0A 73    .|-qN/T.]\'4.#.s
00000440: 76 A5 81 33 22 31 56 6B 1D 82 C4 32 FB 82 15 F6    v%.3"1Vk..D2{..v
00000450: 97 C8 47 29 3C 9E 59 9A C0 83 48 A0 55 CB C8 D6    .HG)<.Y.@.H.UKHV
00000460: 36 92 CC 54 A7 00 E3 28 9E 99 45 B2 E5 7E 88 A7    6.LT'.c(..E2e~.'
00000470: 28 4E CA 75 17 3C D3 B5 6C F5 FD AC 05 55 BF F7    (NJu.<S5lu},.U?w
00000480: 98 61 92 30 D8 0F 0E A5 DD 61 4D 80 27 5B A7 68    .a.0X..%]aM.'['h
00000490: E5 B9 C2 B8 EE 31 F6 63 29 37 C5 C9 11 39 90 8D    e9B8n1vc)7EI.9..
000004a0: D8 00 35 F4 7A 2D 79 D0 6A BB 9C 98 E4 41 CF 3F    X.5tz-yPj;..dAO?
000004b0: DE 9D 8B BF 04 69 1D BC 5C E7 E1 F2 49 01 8D F5    ^..?.i.<\garI..u
000004c0: 41 3E 3F FB AE 54 B2 D9 F2 A0 E8 0A F7 59 47 77    A>?{.T2Yr.h.wYGw
000004d0: 3C 19 C8 7B 81 9B 17 19 E9 81 A0 36 AD C6 62 71    <.H{....i..6-Fbq
000004e0: DB 68 72 8F 6A 37 45 D9 0E 6E DC 2C 5E 52 C2 75    [hr.j7EY.n\,^RBu
000004f0: 51 2F F9 CE 8A 10 12 E9 C8 68 A9 D6 A6 D7 5B 14    Q/yN...iHh)V&W[.
00000500: 11 51 42 FD BE B5 09 56 7F 19 C3 EB A7 A6 DF 6C    .QB}>5.V..Ck'&_l
00000510: 55 A3 11 DC EF 81 C3 CD DD 63 BF 38 F8 5A 4A 45    U#.\o.CM]c?8xZJE
00000520: 33 24 7B A4 55 B3 85 A6 87 75 3B 85 51 5C 03 B7    3${$U3.&.u;.Q\.7

代码更新

第一个数据包在这里查找 第二个数据包在这里查找

根据其中的一条评论,我已经更新了代码以仅读取MDAT盒子以通过套接字从传入的bytes[]检索H264流。现在我只发送MDAT盒子内容(在MDAT盒子后的下一个字节)。

public Map.Entry<Boolean, List<Integer>> hasMdat(byte[] byteArray) {
    for (int i = 0; i < byteArray.length - 3; i++) {
        if (byteArray[i] == (byte) 109 &&
                byteArray[i + 1] == (byte) 100 &&
                byteArray[i + 2] == (byte) 97 &&
                byteArray[i + 3] == (byte) 116) {

            return Map.entry(true, Arrays.asList(i, i + 1, i + 2, i + 3));
        }
    }
    return Map.entry(false, List.of(0));
}

这是我的代码,用于处理字节流

initSocketConnection(new VideoStreamCallback() {
        @Override
        public void onVideoStreamReceived(byte[] bytes) {
           
Map.Entry<Boolean, List<Integer>> b = hasMdat(bytes);
        if (b.getKey()) {
            byte[] b1 = Arrays.copyOfRange(bytes, b.getValue().get(3) + 1, bytes.length);
  //write b1 back to client using spring SSE
            
        }

        }
    });

2
这似乎是一个分段的MP4,而不是一个原始的H264比特流,其中一些专有数据在前面。片段从0x10dmoof之前的第四个字节)开始。视频有效载荷从0x17d开始,但如果忽略容器,将会丢失时间信息。 - Gyan
1
hexdump看起来包含长度前缀的字符串/二进制数据。初始值为01h,不知道为什么,但后面跟着帧:[00 00 00 04h]“H264”和[00 00 00 24h]“<一些guid>”和[D4h]“<一些json>”和[3F 50h]“<mp4/mov文件>”-- 尝试转储它。很可能是MOV或MP4文件(mp4是mov的一个子集)。 - Christoph Rackwitz
@al_mukthar (1)你需要展示一个完整的以十六进制表示的字节序列,例如:01 00 00 00 04 48 32 36 34 ...等等,否则你目前展示的十六进制转储(包括偏移量和UTF文本)对于我们的测试是无用的。 (2)展示第一个接收到的数据包的十六进制序列,它应该包含主要的元数据。 (3)展示第二个接收到的数据包的十六进制序列,它应该包含关键帧的数据(需要显示其他图片)。 (4)在代码结尾处只需要start += 4;,不需要第二个While循环。 (5)如何在Java中播放MP4文件? - VC.One
那么就不要执行任何操作,只需将整个 MP4 文件提取并提供服务。 - Christoph Rackwitz
看不到你所做的,就不能调试它。这并不意味着提供调试Java代码或任何在浏览器中运行的东西。只需查看 [mre] 并记住,“它不能工作”永远无法进行调试。 - Christoph Rackwitz
显示剩余9条评论
2个回答

4
你的十六进制转储看起来像是一部分被分割的MP4,前缀带有一些JSON。 通常H.264使用帧间压缩。 因此,并非每个帧都是完整的帧,而只是两个帧之间的差异。 因此,您无法在任意点解码H.264。 您需要在流中寻找IDR(瞬时解码器刷新)帧。 IDR帧每10-100帧才能传输一次。 现在我正在查看您的“mdat”十六进制转储
00 00 3E E8 6D 64 61 74 00 00 3E DC 41
00 00 3E E8:'mdat'的大小
6D 64 61 74:“mdat”
00 00 3E DC:NAL单元(H.264的一部分)流的大小
41:表示“1”的NAL单元类型(低4位表示NAL单元类型)
类型为“1”的NAL单元是非IDR图片的编码片段。
因此,您的“mdat”不包含IDR(或关键)帧,因此无法解码。
如果您查看JSON,则会得到另一个数据点,指示缺少关键帧(“IsKeyFrame”:false)。
{
"IsInit":false,
"IsAudio":false,
"TotalSeconds":0.06,
"FrameTime":"2023-02-23T04:21:53.531Z",
"SequenceId":1,
"BaseDecodeTime":26587,
"MediaTime":26587,
"IsFrameHidden":false,
"IsKeyFrame":false,
"Id":445,"Generation":1
}

所以,您可以修改代码,在接收到IDR或关键帧后开始解码。但还有一个潜在问题。您可能需要某些元数据来启动通常在分片mp4的'moov'(而不是'moof')部分中找到的H.264解码器。
流元数据称为序列参数集(SPS)和图像参数集(PPS)。在'mdat'中省略SPS和PPS是合法的,因为它通常被分解到'moov'中。
如果缺少SPS/PPS,则仅将'mdat'交给H.264解码器可能无法正常工作。
无论如何 - 如果您的IDR帧前缀带有SPS/PPS,则仍然必须从'mdat'中删除大小字段,并用起始码(00 00 00 01)替换它们。
基本上,您必须将'mdat'中的mp4样式H.264流转换为'AnnexB'样式,然后再将其馈送到解码器中:
您的'mdat':
'mdat' = <size> data[0] | <size> data[1] | ... | <size> data[n] |

用起始代码替换尺寸,并将多个访问单元分解为单个访问单元。
所需解码器输入:
00 00 00 01 data[0]
00 00 00 01 data[1]
...
00 00 00 01 data[n]

如果你的“mdat”不包含SPS / PPS,那么最好的选择是等待或请求分段mp4的“moov”或init部分,并将完整的分段mp4交给ffmpeg处理。我猜测分段MP4的“moov”或init部分将以JSON成员(“IsInit”:true)为前缀。之前的答案{{link1:解码H264流始终返回MF_E_TRANSFORM_NEED_MORE_INPUT}}链接了一个自包含的示例,解析'mdat'并将其交给媒体基础设施进行解码。

3
“我从Genetec相机接收到了可能是H264编码的字节流...” “我需要解码这些传入的H264流以将视频传输到我的前端...”
注意:如果您的“前端”播放系统可以播放MP4,则已经有可播放的文件。无需从MP4字节中提取H.264字节,或将字节转换为AnnexB格式,或添加起始代码,或跳过音频帧等。只需删除起始的Genetec标头,剩余的数据就是可播放的MP4。
以下答案适用于播放系统接受分块MP4数据(包含在MP4中的H.264)。如果播放器期望实际的原始H.264帧,则需要提取,请参见Markus Schumann的答案。
最短版本:从每个数据包中:
  • 从偏移量[49]读取32位整数中可跳过的"大小"(以大端格式)。
  • MP4数据从int mp4_data_pos = (49 + skipSize);开始。
  • 仅提取MP4数据部分,并将其用于作为视频播放的测试。
  • 在MP4数据中:[moov]是元数据,然后块是[moof+mdat]...[moof+mdat]
  • 查找moov0x6D6F6F76,查找moof0x6D6F6F66,查找mdat0x6D646174
  • 通过从前四个字节读取32位整数来获取每个原子的大小。
    (例如:int size_mdat = read_32_bits_from( pos_mdat - 4);,因为一个数组槽可以容纳8位)。
  • 元数据部分后的第一个[moof + mdat]块必须包含用于显示的关键帧。
  • 通过检查帧类型5来找到关键帧:
    int frame_type = ( byteArray[ mdat_pos + 8 ] & 0x1F );
    (NB 1: 如果它是一个块中的第一帧,则只有这个方法才能找到关键帧 (例如:一个 [moof + mdat] 块)。
    (NB 2: 如果没有获得关键帧,可能是由于Genetec设备后面输出了关键帧 (例如:每25帧),或者可能在设备启动时才发出一个关键帧?请检查此类问题。
  • 如果要使用HTML5视频标记进行测试,则您的编解码器设置为:avc1.640028
  • 如果作为文件进行测试,则将N个"MP4数据"部分保存为一个文件(即:文件为连接的MP4块)。

## 简短版本:

[稍后添加:跳过头部段的图像 + 高亮显示 32 位整数]

在每个数据包中...

  • 跳过前49个字节。
  • 在偏移量49处,读取一个32位整数(即:为了更新“skipSize”变量)。
  • 根据skipSize变量的新值向前跳过一些对象字节。
  • MP4数据从这里开始(例如:mp4_begins_pos = (49 + skipSize);)。

有关数据包中的MP4数据的一些注释:

  • MP4数据是到数据包结尾处的,并且可能比当前数据包大小大。
  • 大于数据包自身大小的MP4数据将在下一个数据包中继续。
  • 以字节为单位:moov原子具有所需的MP4元数据,moof原子是可播放的MP4块。
  • 以字节为单位:MP4原子以SIZE(32位或4字节)开头,后跟原子名称。
  • 以字节为单位:按出现顺序将所有原子(根据其SIZE)复制到新的字节数组中。
    可以将此数组保存为文件或发送到解码器进行播放。

(选项A)对于在线播放使用HTML5视频标记):
仅将“MP4数据”部分的数组发送到前端。
(意思是所有数据包字节减去前49个字节对象字节(按其大小))

Web播放意味着选择以下之一:

  • 使用 MediaSource Extensions API 手动将接收到的块提供给浏览器的 MPEG 解码器。

  • 使用服务器端脚本作为标签的 src,其中脚本将 MP4 数据“管道”回视频标签。

  • 在服务器上保存块时,使用 HLS Live Playlist 格式为前端客户端提供服务。

从 STSD 原子中,我可以看到您拥有:H.264 “高”配置文件 @ 级别 4.0(或 avc1.640028)。

type= video/mp4; codecs= "avc1.640028";

(选项B) 为了快速测试,将其保存为文件:
将所需的N个“MP4数据”部分连接(合并)成一个长数组,然后将该数组保存为文件。在像VLC Media Player这样的播放器中从您的存储文件夹中测试新的MP4文件。

## 长版本:

Genetec的MP4头格式很容易理解:

(1)每个数据包都以一些可跳过的49字节值开头。

  • (a) 每个数据包的第一个字节的值为0x01

    • 使用if (byteArray[0] == 1) { /* is OK packet */ }来确认。
    • 否则,假设存在一些数据包损坏。跳过/忽略这些数据包。
  • (b) 随后是32位整数表示数据的大小,然后是数据本身:类型为String(例如:H264)。

    • 示例数据包-1的字节: 00 00 00 04 (=4),然后是 48 32 36 34 (=H264)
  • (c) 随后是32位整数表示可能的GUUID的大小(似乎有足够的十六进制值来表示16个字节)。

    • 示例数据包-1的字节: 00 00 00 24 (=36),然后是以XXXX-XX-XX-etc格式的十六进制值。

以上数据似乎总是49个字节。通过检查其他数据包来确认(是否有一个49个字节的模式,即由“H264”字符串后跟一系列“XXXX-XX-XX-etc”样式值组成?)。

  • 跳过这些49个字节(Genetec的自定义头字节)。
  • 跳过范围为[byte 0] ... [byte 48],因为这些字节不需要用于播放。

(2) 跳过后,有一个Object(由Genetec添加)的侧面元数据(例如:{"IsAudio":false} )。

  • (a)偏移量49开始,读取一个32位整数以获取对象的大小(字节长度)。
    • 示例数据包-1字节:00 00 00 C8 == 是36个字节的大小
  • (b) 使用找到的大小跳过对象字节,以到达示例位置:偏移量N
    • 这些自定义对象的字节不需要用于回放。

(3)偏移量N处,实际的MP4数据开始。

以下是一个起始示例代码,用于检查数据包中的MP4数据大小以提取进行回放。

使用我提供的read_int32函数从某个数组位置获取32位整数。

import java.util.Arrays;

public class Main 
{
    //# Vars for MP4 data
    public static int size_MP4_data = 0;
    public static int size_expected_MP4_data = 0;
    public static int size_received_MP4_data = 0;
    public static int offset_MP4_data = 0;
    
    public static boolean need_more_MP4_data = false;
     
    public static void main(String[] args) 
    {
        //# Example Array to represent a received input Packet
        //# Array contents are from the first 320 bytes of your first example packet
        //# See full bytes at: https://pastebin.com/embed_js/3Ca8ZDFk 
        int[] bytes_Packet =    {

                                    0x01, 0x00, 0x00, 0x00, 0x04, 0x48, 0x32, 0x36, 0x34, 0x00, 0x00, 0x00, 0x24, 0x39, 0x33, 0x65, 
                                    0x63, 0x35, 0x39, 0x31, 0x30, 0x2D, 0x65, 0x65, 0x35, 0x38, 0x2D, 0x34, 0x39, 0x37, 0x32, 0x2D, 
                                    0x61, 0x30, 0x66, 0x66, 0x2D, 0x32, 0x65, 0x62, 0x33, 0x61, 0x33, 0x61, 0x34, 0x32, 0x66, 0x35, 
                                    0x66, 0x00, 0x00, 0x00, 0xC8, 0x7B, 0x22, 0x49, 0x73, 0x49, 0x6E, 0x69, 0x74, 0x22, 0x3A, 0x74, 
                                    0x72, 0x75, 0x65, 0x2C, 0x22, 0x49, 0x73, 0x41, 0x75, 0x64, 0x69, 0x6F, 0x22, 0x3A, 0x66, 0x61, 
                                    0x6C, 0x73, 0x65, 0x2C, 0x22, 0x54, 0x6F, 0x74, 0x61, 0x6C, 0x53, 0x65, 0x63, 0x6F, 0x6E, 0x64, 
                                    0x73, 0x22, 0x3A, 0x30, 0x2E, 0x30, 0x2C, 0x22, 0x46, 0x72, 0x61, 0x6D, 0x65, 0x54, 0x69, 0x6D, 
                                    0x65, 0x22, 0x3A, 0x22, 0x32, 0x30, 0x32, 0x33, 0x2D, 0x30, 0x32, 0x2D, 0x32, 0x35, 0x54, 0x31, 
                                    0x36, 0x3A, 0x35, 0x30, 0x3A, 0x32, 0x37, 0x2E, 0x32, 0x36, 0x31, 0x5A, 0x22, 0x2C, 0x22, 0x53, 
                                    0x65, 0x71, 0x75, 0x65, 0x6E, 0x63, 0x65, 0x49, 0x64, 0x22, 0x3A, 0x31, 0x2C, 0x22, 0x42, 0x61, 
                                    0x73, 0x65, 0x44, 0x65, 0x63, 0x6F, 0x64, 0x65, 0x54, 0x69, 0x6D, 0x65, 0x22, 0x3A, 0x30, 0x2C, 
                                    0x22, 0x4D, 0x65, 0x64, 0x69, 0x61, 0x54, 0x69, 0x6D, 0x65, 0x22, 0x3A, 0x30, 0x2C, 0x22, 0x49, 
                                    0x73, 0x46, 0x72, 0x61, 0x6D, 0x65, 0x48, 0x69, 0x64, 0x64, 0x65, 0x6E, 0x22, 0x3A, 0x66, 0x61,
                                    0x6C, 0x73, 0x65, 0x2C, 0x22, 0x49, 0x73, 0x4B, 0x65, 0x79, 0x46, 0x72, 0x61, 0x6D, 0x65, 0x22, 
                                    0x3A, 0x66, 0x61, 0x6C, 0x73, 0x65, 0x2C, 0x22, 0x49, 0x64, 0x22, 0x3A, 0x30, 0x2C, 0x22, 0x47,
                                    0x65, 0x6E, 0x65, 0x72, 0x61, 0x74, 0x69, 0x6F, 0x6E, 0x22, 0x3A, 0x31, 0x7D, 0x00, 0x00, 0x02,
                                    0xC4, 0x00, 0x00, 0x00, 0x1C, 0x66, 0x74, 0x79, 0x70, 0x64, 0x61, 0x73, 0x68, 0x00, 0x00, 0x00,
                                    0x00, 0x69, 0x73, 0x6F, 0x6D, 0x64, 0x61, 0x73, 0x68, 0x6D, 0x70, 0x34, 0x31, 0x00, 0x00, 0x02, 
                                    0xA8, 0x6D, 0x6F, 0x6F, 0x76, 0x00, 0x00, 0x00, 0x78, 0x6D, 0x76, 0x68, 0x64, 0x01, 0x00, 0x00, 
                                    0x00, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0xEC, 0xD3, 0x00, 0x00, 0x00, 0x00, 0xE0, 0x1F, 0xEC
                                  
                                };

        //# Process the packet to extract MP4 data
        int[] data_MP4 = process_Packet( bytes_Packet ); //# returns a trimmed array
    }
    
    static int[] process_Packet( int[] input ) 
    {
        //# (optional) Confirm function code is running ....
        System.out.println( "## Checking received Packet byte values .... " );
        
        //# NOTE: 
        //# "size_header_Genetec" is the mentioned "skipSize" variable equivalent.
        //# it stores how much bytes length (size) to skip past to reach MP4 data.
        
        int temp_int = 0; //# temp number for counting
        int size_total_packet = input.length; //# using size of "input" packet given to this function
        int size_header_Genetec = 0;
        
        //# first check if this packet's MP4 data needs to be added to another previous packet's data to make a full (uncorrupt) chunk.
        if( need_more_MP4_data == true)
        {
            //## to fix later (if needed)
            //## solution: extract needed remainder and append to an existing array
        }
        
        ///////////////////////////////
        //### Phase 1: Find MP4 attoms
        ///////////////////////////////
        
        //# Account for starting "0x01" byte
        size_header_Genetec = 1;
        
        //# since the size is increased with a "+=" we can re-use
        //# the newly increased "size_header_Genetec" value.
        
        //# get next size (usually String of 4 letters: "H264")
        temp_int = read_int32( input, size_header_Genetec );
        size_header_Genetec += (temp_int + 4);
        
        //# get next size (usually a GUUID of hex values)
        temp_int = read_int32( input, size_header_Genetec );
        size_header_Genetec += (temp_int + 4);
        
        //# get next size (usually an Object of metadata values)
        temp_int = read_int32( input, size_header_Genetec );
        size_header_Genetec += (temp_int + 4);
        
        //# skip next 4 bytes
        size_header_Genetec += 4;
       
        //# Update offset for later use ...
        offset_MP4_data = size_header_Genetec;
        
        //# final check before next steps
        System.out.println( "- MP4 data begins at offset: [" + offset_MP4_data + "] until end of this packet" );
        
        ////////////////////////////////
        //### Phase 2: Handle MP4 atoms
        ////////////////////////////////
        
        //# check atom NAME
        temp_int = read_int32( input, offset_MP4_data + 4 );
        
        if( 
            //# IF atom "name" is one of these (as expected from your packet MP4 structure)...
            ( temp_int == 0x66747970 )    //# is "ftyp"
            || ( temp_int == 0x6D6F6F76 ) //# is "moov"
            || ( temp_int == 0x6D6F6F66 ) //# is "moof"
            || ( temp_int == 0x6D646174 ) //# is "mdat"
            
        )
        {
            //# THEN check all MP4 atoms in this packet for total size of MP4 data
            //# do this by adding together all the atom sizes
            //# note: if size is bigger than packet, then store data for completion with next packet
            
            //# add sizes of MP4 atoms...
            
            temp_int = offset_MP4_data;
                
            while(true)
            {
                size_expected_MP4_data += read_int32( input, temp_int );
                temp_int = ( offset_MP4_data + size_expected_MP4_data );
                
                //# avoid reading past end of packet
                if( size_expected_MP4_data >= ( size_total_packet - offset_MP4_data ) )
                {
                    break;
                }
            }
        
        }
        
        /////////////////////////////////////////////////////////////////////////////
        //# confirm MP4 data positions are correct (double-check by a hex view of same bytes)...
        System.out.println( ">> MP4 data offset: " + offset_MP4_data );
        System.out.println( ">> MP4 data length is : " + size_expected_MP4_data );
        /////////////////////////////////////////////////////////////////////////////
        
        ///////////////////////////////
        //# Phase 3: Copy the MP4 data
        ///////////////////////////////
        
        int copyStartPos = offset_MP4_data;
        int copyEndPos = (size_total_packet-1);
        
        //# prepare for when getting the next new packet
        
        if( size_expected_MP4_data > ( size_total_packet - offset_MP4_data ) ) { need_more_MP4_data = true; }
        
        if( need_more_MP4_data == false )
        {
            //# reset the count for this new MP4 chunk
            //size_received_MP4_data = 0;
            //size_expected_MP4_data = 0;
        }
        
        //# slice the Array to keep only the MP4 data parts
        return ( Arrays.copyOfRange( input , copyStartPos, copyEndPos) );
        
    }
    
    //# function expects:    
    //# "input" = array to search, 
    //# "pos" = start position of reading a 4-byte sequence 
    static int read_int32( int[] input, int pos ) 
    {
        
        //# integer to hold combined 4-byte values as one result/number
        int temp_int = 0;
        
        //# join the four byte values into "temp_int"
        temp_int = ( input[ pos+0 ] << 24 );
        temp_int |= ( input[ pos+1 ] << 16 );
        temp_int |= ( input[ pos+2 ] << 8 );
        temp_int |= ( input[ pos+3 ] << 0 );

        return ( temp_int ); 
        
    }
        
}

流播放的选项:

(a) 如果您的播放器需要MP4数据:

  • 您的数据已经可以在大多数视频播放器中播放
  • 跳过前49字节和 { ... } 对象字节后,发送每个数据包的所有数据。

(b) 如果您的播放器需要原始H.264数据(即它不播放MP4数据,只播放H.264数据):

  • 然后,您需要从MP4数据中提取每个H.264视频帧。
  • 您的H.264数据以AVCC格式(.mp4)存储,这意味着每个帧都有一个“字节大小”值。
  • 大多数播放器都希望原始H.264以AnnexB格式(.h264)存储。
    请使用此四个字节序列00 00 00 01替换帧大小的所有四个字节。
    (正确的做法是:对于SPS、PPS和关键帧,请使用00 00 00 01,然后对于其他帧,请使用00 00 01 例如:P-frames)。

谢谢你的回答,附上代码示例。我会接受这个答案。 - ashif-ismail
1
我已经进行了必要的修改,以删除遗传和自定义对象元数据添加的额外标头,并根据它们的大小进行调整,但仍然无法播放发送到前端的数据包。我尝试将数据包转储到文件中并在VLC中运行,结果显示NAL数据包已损坏。我发现我的关键帧来自套接字的每个第二个数据包。请查看我修改后的数据包转储。 - ashif-ismail
1
1个数据包 => https://pastebin.com/embed_js/b9HnhxGP - ashif-ismail
1
第二个数据包(最后一部分省略)=> https://pastebin.com/embed_js/gdHMeLX0 - ashif-ismail
PS:(我很快就会删除这条评论,请不要回复,只是分享一下个人想法):基本上你的代码现在已经实现了ffmpeg -i genetec.mp4 -c copy normal.mp4所能实现的功能。然而,这个命令在FFmpeg中无法工作,因为它无法理解Genetec头文件,并且会认为输入的MP4文件已损坏。这意味着你的代码正在做FFmpeg无法做到的事情。我的观点是:创造自己的解决方案并非不可能。此外,这个字节切片过程也让你具备了以下技能:(a)从MP3中提取图像,(b)修复损坏的MP4(录制时没有电池),(c)剪辑MP4。 - VC.One
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接