Skip to content

Commit 4325714

Browse files
committed
fu flv
1 parent 9c56959 commit 4325714

File tree

19 files changed

+1068
-230
lines changed

19 files changed

+1068
-230
lines changed

flv/src/commonMain/kotlin/io/github/thibaultbee/krtmp/flv/FLVDemuxer.kt

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@
1616
package io.github.thibaultbee.krtmp.flv
1717

1818
import io.github.thibaultbee.krtmp.amf.AmfVersion
19-
import io.github.thibaultbee.krtmp.flv.models.util.FlvHeader
2019
import io.github.thibaultbee.krtmp.flv.models.tags.FLVTag
20+
import io.github.thibaultbee.krtmp.flv.models.tags.RawFLVTag
21+
import io.github.thibaultbee.krtmp.flv.models.util.FLVHeader
2122
import kotlinx.io.Source
2223
import kotlinx.io.buffered
2324
import kotlinx.io.files.Path
@@ -34,9 +35,9 @@ import kotlinx.io.readString
3435
fun FLVDemuxer(
3536
path: Path,
3637
amfVersion: AmfVersion = AmfVersion.AMF0
37-
): FlvDemuxer {
38+
): FLVDemuxer {
3839
val source = SystemFileSystem.source(path)
39-
return FlvDemuxer(source.buffered(), amfVersion)
40+
return FLVDemuxer(source.buffered(), amfVersion)
4041
}
4142

4243
/**
@@ -45,15 +46,41 @@ fun FLVDemuxer(
4546
* @param source the source to read from
4647
* @param amfVersion the AMF version to use
4748
*/
48-
class FlvDemuxer(private val source: Source, private val amfVersion: AmfVersion = AmfVersion.AMF0) {
49+
class FLVDemuxer(private val source: Source, private val amfVersion: AmfVersion = AmfVersion.AMF0) {
4950
private var hasDecoded = false
5051

5152
/**
52-
* Decodes a single FLV frames.
53+
* Whether the source contains any FLV frame.
54+
*/
55+
val isEmpty: Boolean
56+
get() {
57+
/**
58+
* After decoding the last frame, the PreviousTagSizeN-1 (4 bits) is still there.
59+
* but there isn't a frame to decode.
60+
*/
61+
return !source.request(5)
62+
}
63+
64+
/**
65+
* Decodes a single FLV frames, the data is parsed.
5366
*
5467
* @return the decoded [FLVTag]
5568
*/
5669
fun decode(): FLVTag {
70+
return decode { FLVTag.decode(source, amfVersion) }
71+
}
72+
73+
/**
74+
* Decodes only the raw FLV tag of the next frame.
75+
* The data is not parsed.
76+
*
77+
* @return the decoded [RawFLVTag]
78+
*/
79+
fun decodeRaw(): RawFLVTag {
80+
return decode { RawFLVTag.decode(source) }
81+
}
82+
83+
private fun <T> decode(block: (Source) -> T): T {
5784
val peek = source.peek()
5885
val isHeader = try {
5986
peek.readString(3) == "FLV"
@@ -63,7 +90,7 @@ class FlvDemuxer(private val source: Source, private val amfVersion: AmfVersion
6390

6491
if (isHeader) {
6592
// Skip header
66-
FlvHeader.decode(source)
93+
FLVHeader.decode(source)
6794
}
6895

6996
val previousTagSize = source.readInt()
@@ -72,19 +99,18 @@ class FlvDemuxer(private val source: Source, private val amfVersion: AmfVersion
7299
require(previousTagSize == 0) { "Invalid FlvHeader. Expected PreviousTagSize0 to be 0." }
73100
}
74101

75-
return FLVTag.decode(source, amfVersion)
102+
return block(source)
76103
}
77104

78-
79-
fun decodeFlvHeader(): FlvHeader {
105+
fun decodeFlvHeader(): FLVHeader {
80106
val peek = source.peek()
81107
val isHeader = try {
82108
peek.readString(3) == "FLV"
83109
} catch (e: Exception) {
84110
false
85111
}
86112
if (isHeader) {
87-
return FlvHeader.decode(source)
113+
return FLVHeader.decode(source)
88114
} else {
89115
throw IllegalStateException("Not a FLV header")
90116
}

flv/src/commonMain/kotlin/io/github/thibaultbee/krtmp/flv/FLVMuxer.kt

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@ package io.github.thibaultbee.krtmp.flv
1818
import io.github.thibaultbee.krtmp.amf.AmfVersion
1919
import io.github.thibaultbee.krtmp.flv.models.tags.FLVData
2020
import io.github.thibaultbee.krtmp.flv.models.tags.FLVTag
21+
import io.github.thibaultbee.krtmp.flv.models.tags.RawFLVTag
2122
import io.github.thibaultbee.krtmp.flv.models.tags.aacAudioData
22-
import io.github.thibaultbee.krtmp.flv.models.tags.avcHeaderVideoData
23-
import io.github.thibaultbee.krtmp.flv.models.tags.avcVideoData
24-
import io.github.thibaultbee.krtmp.flv.models.util.FlvHeader
23+
import io.github.thibaultbee.krtmp.flv.models.tags.avcHeaderLegacyVideoData
24+
import io.github.thibaultbee.krtmp.flv.models.tags.avcLegacyVideoData
25+
import io.github.thibaultbee.krtmp.flv.models.util.FLVHeader
2526
import kotlinx.io.Sink
2627
import kotlinx.io.buffered
2728
import kotlinx.io.files.Path
@@ -34,7 +35,7 @@ import kotlinx.io.files.SystemFileSystem
3435
* @param amfVersion the AMF version to use
3536
* @return a [FLVMuxer]
3637
*/
37-
fun FlvMuxer(
38+
fun FLVMuxer(
3839
path: Path,
3940
amfVersion: AmfVersion = AmfVersion.AMF0
4041
): FLVMuxer {
@@ -72,14 +73,32 @@ class FLVMuxer(private val output: Sink, private val amfVersion: AmfVersion = Am
7273
* @param tag the [FLVTag] to encode
7374
*/
7475
fun encode(tag: FLVTag) {
76+
encode(tag.data.getSize(amfVersion) + FLVTag.HEADER_SIZE) { output ->
77+
tag.encode(output, amfVersion)
78+
}
79+
}
80+
81+
/**
82+
* Encodes a [RawFLVTag] to the muxer.
83+
*
84+
* @param tag the [RawFLVTag] to encode
85+
*/
86+
fun encode(tag: RawFLVTag) {
87+
encode(tag.bodySize + FLVTag.HEADER_SIZE) { output ->
88+
tag.encode(output)
89+
}
90+
}
91+
92+
private fun encode(tagSize: Int, block: (Sink) -> Unit) {
7593
if (!hasEncoded) {
7694
hasEncoded = true
7795
output.writeInt(0) // PreviousTagSize0
7896
}
79-
tag.encode(output, amfVersion)
80-
output.writeInt(tag.data.getSize(amfVersion)) // PreviousTagSize
97+
block(output)
98+
output.writeInt(tagSize) // PreviousTagSize
8199
}
82100

101+
83102
/**
84103
* Encodes the FLV header.
85104
*
@@ -93,15 +112,22 @@ class FLVMuxer(private val output: Sink, private val amfVersion: AmfVersion = Am
93112
* @param hasVideo true if the FLV file contains video data, false otherwise
94113
*/
95114
fun encodeFlvHeader(hasAudio: Boolean, hasVideo: Boolean) {
96-
FlvHeader(hasAudio, hasVideo).encode(output)
115+
FLVHeader(hasAudio, hasVideo).encode(output)
116+
}
117+
118+
/**
119+
* Writes all remaining data to the [output] and flushes it.
120+
*/
121+
fun flush() {
122+
output.flush()
97123
}
98124
}
99125

100126
/**
101127
* Encodes a [FLVData] to the muxer.
102128
*
103129
* This method is a convenience method that wraps the [FLVTag] encoding.
104-
* The project comes with [FLVData] factories such as [avcHeaderVideoData], [avcVideoData], [aacAudioData], etc.
130+
* The project comes with [FLVData] factories such as [avcHeaderLegacyVideoData], [avcLegacyVideoData], [aacAudioData], etc.
105131
*
106132
* @param timestampMs the timestamp in milliseconds
107133
* @param data the [FLVData] to encode

flv/src/commonMain/kotlin/io/github/thibaultbee/krtmp/flv/models/config/FourCC.kt

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,11 @@ enum class FourCCs(val value: FourCC) {
3434
)
3535
),
3636
AVC(
37-
FourCC(
37+
AVCHEVCFourCC(
3838
'a', 'v', 'c', '1', MediaType.VIDEO_AVC
3939
)
4040
),
41-
HEVC(FourCC('h', 'v', 'c', '1', MediaType.VIDEO_HEVC));
41+
HEVC(AVCHEVCFourCC('h', 'v', 'c', '1', MediaType.VIDEO_HEVC));
4242

4343
companion object {
4444
fun mimeTypeOf(mediaType: MediaType) =
@@ -51,10 +51,20 @@ enum class FourCCs(val value: FourCC) {
5151
}
5252
}
5353

54+
class AVCHEVCFourCC(
55+
a: Char, b: Char, c: Char, d: Char, mediaType: MediaType
56+
) : FourCC(a, b, c, d, mediaType)
57+
5458
/**
5559
* FourCC is a 4 bytes code used to identify a codec.
5660
*/
57-
data class FourCC(val a: Char, val b: Char, val c: Char, val d: Char, val mediaType: MediaType) {
61+
open class FourCC internal constructor(
62+
val a: Char,
63+
val b: Char,
64+
val c: Char,
65+
val d: Char,
66+
val mediaType: MediaType
67+
) {
5868

5969
/**
6070
* FourCC code

flv/src/commonMain/kotlin/io/github/thibaultbee/krtmp/flv/models/tags/FLVTag.kt

Lines changed: 75 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ package io.github.thibaultbee.krtmp.flv.models.tags
1818
import io.github.thibaultbee.krtmp.amf.AmfVersion
1919
import io.github.thibaultbee.krtmp.amf.internal.utils.readInt24
2020
import io.github.thibaultbee.krtmp.amf.internal.utils.writeInt24
21+
import io.github.thibaultbee.krtmp.flv.models.tags.FLVTag.Type
22+
import io.github.thibaultbee.krtmp.flv.models.util.extensions.readSource
2123
import io.github.thibaultbee.krtmp.flv.models.util.extensions.shl
2224
import kotlinx.io.Buffer
2325
import kotlinx.io.Sink
@@ -36,6 +38,12 @@ class FLVTag(
3638
val data: FLVData,
3739
val streamId: Int = 0,
3840
) {
41+
val type = when (data) {
42+
is AudioData -> Type.AUDIO
43+
is VideoData -> Type.VIDEO
44+
is ScriptDataObject -> Type.SCRIPT
45+
}
46+
3947
/**
4048
* Encodes the FLV tag to the given output stream.
4149
*
@@ -44,11 +52,6 @@ class FLVTag(
4452
*/
4553
fun encode(output: Sink, amfVersion: AmfVersion) {
4654
val isEncrypted = false
47-
val type = when (data) {
48-
is AudioData -> Type.AUDIO
49-
is VideoData -> Type.VIDEO
50-
is ScriptDataObject -> Type.SCRIPT
51-
}
5255
val bodySize = data.getSize(amfVersion)
5356

5457
output.writeByte(((isEncrypted shl 5) or (type.value)).toByte())
@@ -59,9 +62,16 @@ class FLVTag(
5962
data.encode(output, amfVersion, isEncrypted)
6063
}
6164

65+
override fun toString(): String {
66+
return "FLVTag(timestampMs=$timestampMs, data=$data, streamId=$streamId)"
67+
}
68+
6269
companion object {
70+
val HEADER_SIZE = 11 // 11 bytes for the tag header
71+
6372
/**
6473
* Decodes a FLV tag from the given input stream.
74+
* It also decodes the body of the tag.
6575
*
6676
* @param source The input stream to read the FLV tag from.
6777
* @param amfVersion The AMF version to use for decoding.
@@ -71,15 +81,15 @@ class FLVTag(
7181
val isEncrypted = (flags and 0x20) != 0
7282
val type = Type.entryOf(flags and 0x1F)
7383
val bodySize = source.readInt24()
74-
val timestamp = source.readInt24() or (source.readByte().toInt() shl 24)
84+
val timestampMs = source.readInt24() or (source.readByte().toInt() shl 24)
7585
val streamId = source.readInt24() // Stream ID
7686

7787
val data = when (type) {
7888
Type.AUDIO -> AudioData.decode(source, bodySize, isEncrypted)
7989
Type.VIDEO -> VideoData.decode(source, bodySize, isEncrypted)
8090
Type.SCRIPT -> ScriptDataObject.decode(source, amfVersion)
8191
}
82-
return FLVTag(timestamp, data, streamId)
92+
return FLVTag(timestampMs, data, streamId)
8393
}
8494
}
8595

@@ -89,7 +99,8 @@ class FLVTag(
8999
SCRIPT(18);
90100

91101
companion object {
92-
fun entryOf(value: Int) = entries.first { it.value == value }
102+
fun entryOf(value: Int) = entries.firstOrNull { it.value == value }
103+
?: throw IllegalArgumentException("Unknown FLV tag type: $value")
93104
}
94105
}
95106
}
@@ -114,5 +125,61 @@ fun FLVTag.readByteArray(amfVersion: AmfVersion = AmfVersion.AMF0): ByteArray {
114125
return readBuffer(amfVersion).readByteArray()
115126
}
116127

128+
/**
129+
* Represents a raw FLV tag. Raw means that the body is not decoded.
130+
*/
131+
class RawFLVTag internal constructor(
132+
val isEncrypted: Boolean,
133+
val type: Type,
134+
val bodySize: Int,
135+
val timestampMs: Int,
136+
val body: Source,
137+
val streamId: Int = 0
138+
) {
139+
fun peek() = RawFLVTag(isEncrypted, type, bodySize, timestampMs, body.peek(), streamId)
140+
141+
142+
fun decode(amfVersion: AmfVersion = AmfVersion.AMF0): FLVTag {
143+
val data = when (type) {
144+
Type.AUDIO -> AudioData.decode(body, bodySize, isEncrypted)
145+
Type.VIDEO -> VideoData.decode(body, bodySize, isEncrypted)
146+
Type.SCRIPT -> ScriptDataObject.decode(body, amfVersion)
147+
}
148+
return FLVTag(timestampMs, data, streamId)
149+
}
150+
151+
fun encode(output: Sink) {
152+
output.writeByte(((isEncrypted shl 5) or (type.value)).toByte())
153+
output.writeInt24(bodySize)
154+
output.writeInt24(timestampMs)
155+
output.writeByte((timestampMs shr 24).toByte())
156+
output.writeInt24(streamId) // Stream ID
157+
output.write(body, bodySize.toLong())
158+
}
159+
160+
override fun toString(): String {
161+
return "RawFLVTag(type=$type, timestampMs=$timestampMs, streamId=$streamId, bodySize=$bodySize)"
162+
}
163+
164+
companion object {
165+
/**
166+
* Decodes a RAW FLV tag from the given input stream.
167+
* It does not decode the body of the tag.
168+
*
169+
* @param source The input stream to read the FLV tag from.
170+
*/
171+
fun decode(source: Source): RawFLVTag {
172+
val flags = source.readByte().toInt()
173+
val isEncrypted = (flags and 0x20) != 0
174+
val type = Type.entryOf(flags and 0x1F)
175+
val bodySize = source.readInt24()
176+
val timestampMs = source.readInt24() or (source.readByte().toInt() shl 24)
177+
val streamId = source.readInt24() // Stream ID
178+
val body = source.readSource(bodySize.toLong())
179+
180+
return RawFLVTag(isEncrypted, type, bodySize, timestampMs, body, streamId)
181+
}
182+
}
183+
}
117184

118185

0 commit comments

Comments
 (0)