diff --git a/src/main/java/io/antmedia/muxer/HLSMuxer.java b/src/main/java/io/antmedia/muxer/HLSMuxer.java index de7621432..1449cd3b5 100644 --- a/src/main/java/io/antmedia/muxer/HLSMuxer.java +++ b/src/main/java/io/antmedia/muxer/HLSMuxer.java @@ -277,26 +277,37 @@ public synchronized void writeMetaData(String data, long dts) { addID3Data(data); } + + public static byte[] convertIntToID3v2TagSize(int size) { + byte[] tagSizeBytes = new byte[4]; + tagSizeBytes[0] = (byte) ((size >> 21) & 0x7F); + tagSizeBytes[1] = (byte) ((size >> 14) & 0x7F); + tagSizeBytes[2] = (byte) ((size >> 7) & 0x7F); + tagSizeBytes[3] = (byte) (size & 0x7F); + return tagSizeBytes; + } + public synchronized void addID3Data(String data) { - - - int id3TagSize = data.length() + 3; // TXXX frame size (excluding 10 byte header) - int tagSize = id3TagSize + 10; + int frameSizeWithoutFrameHeader = data.length() + 3; // TXXX frame size, 3 is for encoding (1), description (1) and end of string (1) (https://id3.org/id3v2.3.0#User_defined_text_information_frame) + int tagSize = frameSizeWithoutFrameHeader + 10; // 10 is for frame header which is "TXXX" frame id (4), frame size info(4) and frame flags (2) (https://id3.org/id3v2.3.0#ID3v2_frame_overview) + int id3ContentSize = tagSize + 10; // 10 is for ID3 header which is "ID3" (3), version (2), flags (1) and size info(4) (https://id3.org/id3v2.3.0#ID3v2_header) - ByteBuffer byteBuffer = ByteBuffer.allocate(tagSize + 10); + ByteBuffer byteBuffer = ByteBuffer.allocate(id3ContentSize); logger.info("Adding ID3 data: {} lenght:{} byte length:{} buffer capacacity:{}", data, data.length(), data.getBytes().length, byteBuffer.capacity()); - + // ID3 header (https://id3.org/id3v2.3.0#ID3v2_header) byteBuffer.put("ID3".getBytes()); byteBuffer.put(new byte[]{0x03, 0x00}); // version byteBuffer.put((byte) 0x00); // flags - byteBuffer.putInt(tagSize); // size + byteBuffer.put(convertIntToID3v2TagSize(tagSize)); // size + + // TXXX frame header (https://id3.org/id3v2.3.0#ID3v2_frame_overview) + byteBuffer.put("TXXX".getBytes()); // frame id + byteBuffer.putInt(frameSizeWithoutFrameHeader); // frame size without frame header + byteBuffer.put(new byte[]{0x00, 0x00}); // frame flags - // TXXX frame - byteBuffer.put("TXXX".getBytes()); - byteBuffer.putInt(id3TagSize); // size - byteBuffer.put(new byte[]{0x00, 0x00}); // flags + //TXXX frame content (https://id3.org/id3v2.3.0#User_defined_text_information_frame) byteBuffer.put((byte) 0x03); // encoding byteBuffer.put((byte) 0x00); // description 00 byteBuffer.put(data.getBytes()); // description diff --git a/src/test/java/io/antmedia/test/MuxerUnitTest.java b/src/test/java/io/antmedia/test/MuxerUnitTest.java index 226928cbe..2263ad884 100644 --- a/src/test/java/io/antmedia/test/MuxerUnitTest.java +++ b/src/test/java/io/antmedia/test/MuxerUnitTest.java @@ -32,13 +32,7 @@ import static org.bytedeco.ffmpeg.global.avutil.AV_SAMPLE_FMT_FLTP; import static org.bytedeco.ffmpeg.global.avutil.av_channel_layout_default; import static org.bytedeco.ffmpeg.global.avutil.av_dict_get; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertNull; -import static org.junit.Assert.assertTrue; -import static org.junit.Assert.fail; +import static org.junit.Assert.*; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; @@ -5681,4 +5675,106 @@ public void testRtmpDtsOverflow() { 0, 0, false, 0, lastVideoDts + (long) overFlowCount * Integer.MAX_VALUE); } + + @Test + public void testID3HeaderTagSize() { + int size = 257; + byte[] tagSizeBytes = HLSMuxer.convertIntToID3v2TagSize(size); + for (byte b : tagSizeBytes) { + System.out.printf("%02X ", b); // Print bytes in hexadecimal format + } + + assertEquals(0x00, tagSizeBytes[0]); + assertEquals(0x00, tagSizeBytes[1]); + assertEquals(0x02, tagSizeBytes[2]); + assertEquals(0x01, tagSizeBytes[3]); + + + tagSizeBytes = HLSMuxer.convertIntToID3v2TagSize((int) Math.pow(2, 7)); + + assertEquals(0x00, tagSizeBytes[0]); + assertEquals(0x00, tagSizeBytes[1]); + assertEquals(0x01, tagSizeBytes[2]); + assertEquals(0x00, tagSizeBytes[3]); + + tagSizeBytes = HLSMuxer.convertIntToID3v2TagSize((int) Math.pow(2, 14)); + + assertEquals(0x00, tagSizeBytes[0]); + assertEquals(0x01, tagSizeBytes[1]); + assertEquals(0x00, tagSizeBytes[2]); + assertEquals(0x00, tagSizeBytes[3]); + + tagSizeBytes = HLSMuxer.convertIntToID3v2TagSize((int) Math.pow(2, 21)); + + assertEquals(0x01, tagSizeBytes[0]); + assertEquals(0x00, tagSizeBytes[1]); + assertEquals(0x00, tagSizeBytes[2]); + assertEquals(0x00, tagSizeBytes[3]); + } + + @Test + public void testAddID3Data() { + HLSMuxer hlsMuxer = spy(new HLSMuxer(vertx, Mockito.mock(StorageClient.class), + "streams", 0, "http://example.com", false)); + hlsMuxer.setId3Enabled(true); + hlsMuxer.createID3StreamIfRequired(); + long lastPts = RandomUtils.nextLong(); + doReturn(lastPts).when(hlsMuxer).getLastPts(); + doNothing().when(hlsMuxer).writeDataFrame(any(), any()); + + int dataSize = 257 - 10 - 3; + String data = "a".repeat(dataSize); // Create a string with 247 'a' characters + + hlsMuxer.addID3Data(data); + + // Capture the parameter passed to writeID3Packet + ArgumentCaptor captor = ArgumentCaptor.forClass(ByteBuffer.class); + verify(hlsMuxer).writeID3Packet(captor.capture()); + + ByteBuffer capturedBuffer = captor.getValue(); + + // Extract values from the captured buffer + byte[] id3Header = new byte[3]; + capturedBuffer.get(id3Header); + assertArrayEquals("ID3".getBytes(), id3Header); + + byte[] version = new byte[2]; + capturedBuffer.get(version); + assertArrayEquals(new byte[]{0x03, 0x00}, version); + + byte flags = capturedBuffer.get(); + assertEquals(0x00, flags); + + byte[] size = new byte[4]; + capturedBuffer.get(size); + assertEquals(0x00, size[0]); + assertEquals(0x00, size[1]); + assertEquals(0x02, size[2]); + assertEquals(0x01, size[3]); + + byte[] frameId = new byte[4]; + capturedBuffer.get(frameId); + assertArrayEquals("TXXX".getBytes(), frameId); + + int frameSize = capturedBuffer.getInt(); + assertEquals(dataSize + 3, frameSize); + + byte[] frameFlags = new byte[2]; + capturedBuffer.get(frameFlags); + assertArrayEquals(new byte[]{0x00, 0x00}, frameFlags); + + byte encoding = capturedBuffer.get(); + assertEquals(0x03, encoding); + + byte descriptionTerminator = capturedBuffer.get(); + assertEquals(0x00, descriptionTerminator); + + byte[] description = new byte[dataSize]; + capturedBuffer.get(description); + assertArrayEquals(data.getBytes(), description); + + byte endOfString = capturedBuffer.get(); + assertEquals(0x00, endOfString); + } + }