Compare commits

...

10 commits

14 changed files with 718 additions and 20 deletions

51
AVCVideoStream.go Normal file
View file

@ -0,0 +1,51 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
type AVCSampleEntry struct {
*VisualSampleEntry
// TODO: some more
}
// BoxTypeAVCSampleEntry1 AVC Sample Entry Box
const BoxTypeAVCSampleEntry1 = "avc1"
// BoxTypeAVCSampleEntry3 AVC Sample Entry Box
const BoxTypeAVCSampleEntry3 = "avc3"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeAVCSampleEntry1,
ParentTypes: []string{BoxTypeSampleDescription},
Parser: ParseAVCSampleEntry,
}, BoxDefinition{
Type: BoxTypeAVCSampleEntry3,
ParentTypes: []string{BoxTypeSampleDescription},
Parser: ParseAVCSampleEntry,
})
}
// ParseAVCSampleEntry creates a new AVC sample entry box struct based on bytes
func ParseAVCSampleEntry(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
sampleEntry, position := ParseVisualSampleEntry(filePosition, headerSize, content)
box := &AVCSampleEntry{
VisualSampleEntry: sampleEntry,
}
// TODO: parse other fields
_ = position
return box, nil
}

87
ChunkOffsetBox.go Normal file
View file

@ -0,0 +1,87 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
import (
"encoding/binary"
"fmt"
)
// ChunkOffsetBox chunk offset box struct
//
// 8.7.5 Chunk Offset Box
//
// Box Type: stco, co64
// Container: Sample Table Box (stbl)
// Mandatory: Yes
// Quantity: Exactly one variant must be present
//
// The chunk offset table gives the index of each chunk into the containing file. There are two variants,
// permitting the use of 32bit or 64bit offsets. The latter is useful when managing very large
// presentations. At most one of these variants will occur in any single instance of a sample table.
//
// Offsets are file offsets, not the offset into any box within the file (e.g. Media Data Box). This permits
// referring to media data in files without any box structure. It does also mean that care must be taken
// when constructing a selfcontained ISO file with its metadata (Movie Box) at the front, as the size of the
// Movie Box will affect the chunk offsets to the media data.
type ChunkOffsetBox struct {
*FullBox
// is an integer that gives the number of entries in the following table
EntryCount uint32
EntryList []ChunkOffsetEntry
}
type ChunkOffsetEntry struct {
// is a 32 or 64 bit integer that gives the offset of the start of a chunk into its
// containing media file.
ChunkOffset uint32
}
// BoxTypeChunkOffset Chunk Offset Box
const BoxTypeChunkOffset = "stco"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeChunkOffset,
ParentTypes: []string{BoxTypeSampleTable},
Parser: ParseChunkOffsetBox,
})
}
// ParseChunkOffsetBox creates a new chunk offset box struct based on bytes
func ParseChunkOffsetBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &ChunkOffsetBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
// parse entry counter
box.EntryCount = binary.BigEndian.Uint32(content[4:8])
// parse offset entries
position := 8
for i := 0; i < int(box.EntryCount); i++ {
newOffsetEntry := ChunkOffsetEntry{
ChunkOffset: binary.BigEndian.Uint32(content[position+(i*4) : position+4+(i*4)]),
}
box.EntryList = append(box.EntryList, newOffsetEntry)
}
// validate entries amount
if len(box.EntryList) != int(box.EntryCount) {
return box, fmt.Errorf("invalid amount of offset entries at %d; got %d but expected %d", filePosition, len(box.EntryList), box.EntryCount)
}
return box, nil
}

View file

@ -0,0 +1,95 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
import (
"encoding/binary"
"fmt"
)
// TimeToSampleBox time to sample box struct
//
// 8.6.1.2 Decoding Time to Sample Box
//
// Box Type: stts
// Container: Sample Table Box (stbl)
// Mandatory: Yes
// Quantity: Exactly one
//
// This box contains a compact version of a table that allows indexing from decoding time to sample
// number. Other tables give sample sizes and pointers, from the sample number. Each entry in the table
// gives the number of consecutive samples with the same time delta, and the delta of those samples. By
// adding the deltas a complete timetosample map may be built.
//
// The Decoding Time to Sample Box contains decode time delta's: DT(n+1) = DT(n) + STTS(n) where
// STTS(n) is the (uncompressed) table entry for sample n.
//
// The sample entries are ordered by decoding time stamps; therefore the deltas are all nonnegative.
//
// The DT axis has a zero origin; DT(i) = SUM(for j=0 to i1 of delta(j)), and the sum of all deltas gives the
// length of the media in the track (not mapped to the overall timescale, and not considering any edit list).
//
// The Edit List Box provides the initial CT value if it is nonempty (nonzero).
type TimeToSampleBox struct {
*FullBox
// is an integer that gives the number of entries in the following table.
EntryCount uint32
Entries []TimeToSampleBoxEntry
}
type TimeToSampleBoxEntry struct {
// s an integer that counts the number of consecutive samples that have the given duration.
SampleCount uint32
// is an integer that gives the delta of these samples in the timescale of the media.
SampleDelta uint32
}
// BoxTypeDecodingTimeToSample Decoding Time To Sample Box
const BoxTypeDecodingTimeToSample = "stts"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeDecodingTimeToSample,
ParentTypes: []string{BoxTypeSampleTable},
Parser: ParseDecodingTimeToSampleBox,
})
}
// ParseDecodingTimeToSampleBox creates a new decoding time to sample box struct based on bytes
func ParseDecodingTimeToSampleBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &TimeToSampleBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
// parse entry counter
box.EntryCount = binary.BigEndian.Uint32(content[4:8])
// parse sample entries
position := 8
for i := 0; i < int(box.EntryCount); i++ {
newSampleEntry := TimeToSampleBoxEntry{
SampleCount: binary.BigEndian.Uint32(content[position+(i*8) : position+4+(i*8)]),
SampleDelta: binary.BigEndian.Uint32(content[position+4+(i*8) : position+8+(i*8)]),
}
box.Entries = append(box.Entries, newSampleEntry)
}
// validate entries amount
if len(box.Entries) != int(box.EntryCount) {
return box, fmt.Errorf("invalid amount of sample entries at %d; got %d but expected %d", filePosition, len(box.Entries), box.EntryCount)
}
return box, nil
}

View file

@ -14,7 +14,9 @@
package gomp4
import "encoding/binary"
import (
"encoding/binary"
)
type Fixed88 struct {
b []byte
@ -63,7 +65,7 @@ func (fixed Fixed1616) ToFloat32() float32 {
b := binary.BigEndian.Uint16(fixed.b[2:4])
result += float32(a)
result += float32(b) / 100_000 // max of uint16 is 65535
result += float32(b) / 65535 // max of uint16 is 65535
return result
}

55
MovieExtendsBox.go Normal file
View file

@ -0,0 +1,55 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
// MovieExtendsBox Movie Extends Box struct
//
// 8.8.1 Movie Extends Box
//
// Box Type: mvex
// Container: Movie Box (moov)
// Mandatory: No
// Quantity: Zero or one
//
// This box warns readers that there might be Movie Fragment Boxes in this file. To know of all samples in
// the tracks, these Movie Fragment Boxes must be found and scanned in order, and their information
// logically added to that found in the Movie Box.
type MovieExtendsBox struct {
*Box
ChildBoxes []any
}
// BoxTypeMovieExtends Movie Extends Box
const BoxTypeMovieExtends = "mvex"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeMovieExtends,
ParentTypes: []string{BoxTypeMovie},
Parser: ParseMovieExtendsBox,
})
}
// ParseMovieExtendsBox creates a new movie extends box struct based on bytes
func ParseMovieExtendsBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &MovieExtendsBox{
Box: &Box{filePosition, headerSize},
}
// parse child boxes
var err error
box.ChildBoxes, err = box.parseChildBoxes(parser, BoxTypeMovieExtends, filePosition, content)
return box, err
}

View file

@ -26,6 +26,7 @@ import (
type Parser struct {
reader io.Reader
childBoxDefinitions map[string]map[string]BoxDefinition
IgnoreUnknownBoxes bool
Content []any
}
@ -119,7 +120,9 @@ func parseNextBox(parser *Parser, reader io.Reader, currentBoxType string, fileP
box, err = boxTypeDefinition.Parser(parser, filePosition, boxHeaderSize, boxContentBytes)
} else {
logger.Println("unknown box type", boxType, "at", filePosition, "in", currentBoxType)
return nil, filePosition, false, errors.New("unknown box type " + boxType + " in " + currentBoxType)
if !parser.IgnoreUnknownBoxes {
return nil, filePosition, false, errors.New("unknown box type " + boxType + " in " + currentBoxType)
}
}
// check for box errors

View file

@ -29,7 +29,7 @@ func TestMain(m *testing.M) {
}
func TestParser(t *testing.T) {
parser, err := testFile(t, "test/fmp4.mp4")
parser, err := testFile(t, "test/fmp4.mp4", false)
if err != nil {
t.Error(err)
}
@ -45,6 +45,13 @@ func TestParser(t *testing.T) {
// TODO: validate movie fragment header data
}
func TestParserFragmentedMP4Init(t *testing.T) {
_, err := testFile(t, "test/fmp4_init.mp4", true)
if err != nil {
t.Error(err)
}
}
func validateFileType(t *testing.T, parser *Parser) {
// get first file type box
fileTypeBox := parser.Content[0].(*FileTypeBox)
@ -63,12 +70,12 @@ func validateFileType(t *testing.T, parser *Parser) {
}
func TestParserInvalidSampleCount(t *testing.T) {
if _, err := testFile(t, "test/fmp4-incorrect-sample-count.mp4"); err == nil {
if _, err := testFile(t, "test/fmp4-incorrect-sample-count.mp4", false); err == nil {
t.Error("missing error of invalid sample count of track fragment run box")
}
}
func testFile(t *testing.T, filePath string) (*Parser, error) {
func testFile(t *testing.T, filePath string, allowIgnore bool) (*Parser, error) {
// open test file
file, err := os.Open(filePath)
if err != nil {
@ -78,6 +85,7 @@ func testFile(t *testing.T, filePath string) (*Parser, error) {
// create new parser
parser := NewParser(file)
parser.IgnoreUnknownBoxes = allowIgnore
err = parser.Parse()
return parser, err
}

View file

@ -3,7 +3,7 @@
[![pipeline status](https://gitlab.com/martinr92/gomp4/badges/main/pipeline.svg)](https://gitlab.com/martinr92/gomp4/commits/main)
[![coverage report](https://gitlab.com/martinr92/gomp4/badges/main/coverage.svg)](https://gitlab.com/martinr92/gomp4/commits/main)
mp4 implementation in golang based on spec ISO ICE 14496-12:2015
mp4 implementation in golang based on spec ISO/IEC 14496-12:2015
## Parser
@ -16,7 +16,7 @@ if err := parser.Parse(); err != nil {
## Progress
Implementation progress of ISO ICE 14496-12:2015:
Implementation progress of ISO/IEC 14496-12:2015:
| Chapter | Box Types | Parser |
|----------------------------------------------------|----------------|-------:|
@ -39,7 +39,7 @@ Implementation progress of ISO ICE 14496-12:2015:
| 8.5.1 Sample Table Box | stbl | 100% |
| 8.5.2 Sample Description Box | stsd | 100% |
| 8.5.3 Degradation Priority Box | stdp | - |
| 8.6.1.2 Decoding Time to Sample Box | stts | - |
| 8.6.1.2 Decoding Time to Sample Box | stts | 100% |
| 8.6.1.3 Composition Time to Sample Box | ctts | - |
| 8.6.1.4 Composition to Decode Box | cslg | - |
| 8.6.2 Sync Sample Box | stss | - |
@ -49,16 +49,16 @@ Implementation progress of ISO ICE 14496-12:2015:
| 8.6.6 Edit List Box | elst | - |
| 8.7.1 Data Information Box | dinf | 100% |
| 8.7.2 Data Reference Box | dref, url, urn | 100% |
| 8.7.3 Sample Size Boxes | stsz, stz2 | - |
| 8.7.4 Sample To Chunk Box | stsc | - |
| 8.7.5 Chunk Offset Box | stco, co64 | - |
| 8.7.3 Sample Size Boxes | stsz, stz2 | 50% |
| 8.7.4 Sample To Chunk Box | stsc | 100% |
| 8.7.5 Chunk Offset Box | stco, co64 | 50% |
| 8.7.6 Padding Bits Box | padb | - |
| 8.7.7 Sub-Sample Information Box | subs | - |
| 8.7.8 Sample Auxiliary Information Sizes Box | saiz | - |
| 8.7.9 Sample Auxiliary Information Offsets Box | saio | - |
| 8.8.1 Movie Extends Box | mvex | - |
| 8.8.1 Movie Extends Box | mvex | 100% |
| 8.8.2 Movie Extends Header Box | mehd | - |
| 8.8.3 Track Extends Box | trex | - |
| 8.8.3 Track Extends Box | trex | 100% |
| 8.8.4 Movie Fragment Box | moof | 100% |
| 8.8.5 Movie Fragment Header Box | mfhd | 100% |
| 8.8.6 Track Fragment Box | traf | 100% |
@ -67,7 +67,7 @@ Implementation progress of ISO ICE 14496-12:2015:
| 8.8.9 Movie Fragment Random Access Box | mfra | - |
| 8.8.10 Track Fragment Random Access Box | tfra | - |
| 8.8.11 Movie Fragment Random Access Offset Box | mfro | - |
| 8.8.12 Track fragment decode time | tfdt | - |
| 8.8.12 Track fragment decode time | tfdt | 100% |
| 8.8.13 Level Assignment Box | leva | - |
| 8.8.15 Track Extension Properties Box | trep | - |
| 8.8.16 Alternative Startup Sequence Properties Box | assp | - |
@ -118,6 +118,12 @@ Implementation progress of ISO ICE 14496-12:2015:
| 12.2.7 Audio stream loudness | ludt | - |
| 12.4.2 Hint Media Header Box | hmhd | - |
Implementation progress of ISO/IEC 14496-15:2017:
| Chapter | Box Types | Parser |
|----------------------------------------------------|------------------------------------------|-------:|
| 5.4.2 AVC video stream definition | avc1, avc2, avc3, avc4, avcC, m4ds, btrt | 10% |
## Helper Tools
- [Online MP4 file parser](https://www.onlinemp4parser.com/)

View file

@ -17,7 +17,6 @@ package gomp4
import (
"encoding/binary"
"fmt"
"log"
)
// SampleDescriptionBox sample description box struct
@ -112,17 +111,85 @@ type SampleEntry struct {
DataReferenceIndex uint16
}
func ParseSampleEntry(filePosition uint64, headerSize uint32, content []byte) *SampleEntry {
func ParseSampleEntry(filePosition uint64, headerSize uint32, content []byte) (*SampleEntry, int) {
box := &SampleEntry{
Box: &Box{filePosition, headerSize},
}
// skip reserved
position := 2 * 8
position := 6
// parse reference index
box.DataReferenceIndex = binary.BigEndian.Uint16(content[position : position+2])
log.Println("data reference index", box.DataReferenceIndex) // TODO: remove me
position += 2
return box
return box, position
}
// VisualSampleEntry visual sample entry struct
//
// 12.1.3 Sample entry
//
// Video tracks use VisualSampleEntry.
//
// In video tracks, the frame_count field must be 1 unless the specification for the media format explicitly
// documents this template field and permits larger values. That specification must document both how
// the individual frames of video are found (their size information) and their timing established. That
// timing might be as simple as dividing the sample duration by the frame count to establish the frame
// duration.
type VisualSampleEntry struct {
*SampleEntry
Width uint16
Height uint16
// give the resolution of the image in pixelsperinch, as a fixed 16.16 number
HorizResolution Fixed1616
// give the resolution of the image in pixelsperinch, as a fixed 16.16 number
VertResolution Fixed1616
// indicates how many frames of compressed video are stored in each sample. The
// default is 1, for one frame per sample; it may be more than 1 for multiple frames per sample
FrameCount uint16
// is a name, for informative purposes. It is formatted in a fixed 32byte field, with
//the first byte set to the number of bytes to be displayed, followed by that number of bytes of
//displayable data, and then padding to complete 32 bytes total (including the size byte). The field
//may be set to 0.
CompressorName string
Depth uint16
}
func ParseVisualSampleEntry(filePosition uint64, headerSize uint32, content []byte) (*VisualSampleEntry, int) {
sampleEntry, position := ParseSampleEntry(filePosition, headerSize, content)
box := &VisualSampleEntry{
SampleEntry: sampleEntry,
}
// skip pre-defined and reserved bytes
position += 2 + 2 + 4*3
// parse resolution
box.Width = binary.BigEndian.Uint16(content[position : position+2])
box.Height = binary.BigEndian.Uint16(content[position+2 : position+4])
box.HorizResolution = NewFixed1616ByBytes(content[position+4 : position+8])
box.VertResolution = NewFixed1616ByBytes(content[position+8 : position+12])
position += 12
// skip reserved bytes
position += 4
// parse frame count
box.FrameCount = binary.BigEndian.Uint16(content[position : position+2])
position += 2
// parse compressor name length (32bytes total)
compressorNameLength := int(content[position])
box.CompressorName = string(content[position+1 : position+1+compressorNameLength])
position += 32
// parse depth
box.Depth = binary.BigEndian.Uint16(content[position : position+2])
position += 2
// skip pre-defined bytes
position += 2
return box, position
}

87
SampleSizeBox.go Normal file
View file

@ -0,0 +1,87 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
import (
"encoding/binary"
)
// SampleSizeBox sample size box struct
//
// Box Type: stsz, stz2
// Container: Sample Table Box (stbl)
// Mandatory: Yes
// Quantity: Exactly one variant must be present
//
// This box contains the sample count and a table giving the size in bytes of each sample. This allows the
// media data itself to be unframed. The total number of samples in the media is always indicated in the
// sample count.
//
// There are two variants of the sample size box. The first variant has a fixed size 32bit field for
// representing the sample sizes; it permits defining a constant size for all samples in a track. The second
// variant permits smaller size fields, to save space when the sizes are varying but small. One of these
// boxes must be present; the first version is preferred for maximum compatibility.
type SampleSizeBox struct {
*FullBox
// is integer specifying the default sample size. If all the samples are the same size,
// this field contains that size value. If this field is set to 0, then the samples have different sizes,
// and those sizes are stored in the sample size table. If this field is not 0, it specifies the constant
// sample size, and no array follows.
SampleSize uint32
// is an integer that gives the number of samples in the track; if samplesize is 0, then
// it is also the number of entries in the following table.
SampleCount uint32
// is an integer specifying the size of a sample, indexed by its number.
SampleSizeList []SampleSizeEntry
}
type SampleSizeEntry struct {
EntrySize uint32
}
// BoxTypeSampleSizeBox Sample Size Box
const BoxTypeSampleSizeBox = "stsz"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeSampleSizeBox,
ParentTypes: []string{BoxTypeSampleTable},
Parser: ParseSampleSizeBox,
})
}
// ParseSampleSizeBox creates a new sample size box struct based on bytes
func ParseSampleSizeBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &SampleSizeBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
// parse fields
box.SampleSize = binary.BigEndian.Uint32(content[4:8])
box.SampleCount = binary.BigEndian.Uint32(content[8:12])
position := 12
// parse sample sizes
if box.SampleSize == 0 {
for i := 0; i < int(box.SampleCount); i++ {
sampleEntry := SampleSizeEntry{
EntrySize: binary.BigEndian.Uint32(content[position+(i*4) : position+4+(i*4)]),
}
box.SampleSizeList = append(box.SampleSizeList, sampleEntry)
}
}
return box, nil
}

97
SampleToChunkBox.go Normal file
View file

@ -0,0 +1,97 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
import (
"encoding/binary"
"fmt"
)
// SampleToChunkBox sample to chunk box struct
//
// 8.7.4 Sample To Chunk Box
//
// Box Type: stsc
// Container: Sample Table Box (stbl)
// Mandatory: Yes
// Quantity: Exactly one
//
// Samples within the media data are grouped into chunks. Chunks can be of different sizes, and the
// samples within a chunk can have different sizes. This table can be used to find the chunk that contains a
// sample, its position, and the associated sample description.
//
// The table is compactly coded. Each entry gives the index of the first chunk of a run of chunks with the
// same characteristics. By subtracting one entry here from the previous one, you can compute how many
// chunks are in this run. You can convert this to a sample count by multiplying by the appropriate
// samplesperchunk.
type SampleToChunkBox struct {
*FullBox
// is an integer that gives the number of entries in the following table
EntryCount uint32
Entries []SampleToChunkBoxEntry
}
type SampleToChunkBoxEntry struct {
// is an integer that gives the index of the first chunk in this run of chunks that share
// the same samplesperchunk and sampledescriptionindex; the index of the first chunk in a
// track has the value 1 (the first_chunk field in the first record of this box has the value 1,
// identifying that the first sample maps to the first chunk).
FirstChunk uint32
// is an integer that gives the number of samples in each of these chunks
SamplesPerChunk uint32
// is an integer that gives the index of the sample entry that
// describes the samples in this chunk. The index ranges from 1 to the number of sample entries in
// the Sample Description Box
SampleDescriptionIndex uint32
}
// BoxTypeSampleToChunk Sample To Chunk Box
const BoxTypeSampleToChunk = "stsc"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeSampleToChunk,
ParentTypes: []string{BoxTypeSampleTable},
Parser: ParseSampleToChunkBox,
})
}
// ParseSampleToChunkBox creates a new sample to chunk box struct based on bytes
func ParseSampleToChunkBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &SampleToChunkBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
// parse entry counter
box.EntryCount = binary.BigEndian.Uint32(content[4:8])
// parse entries
position := 8
for i := 0; i < int(box.EntryCount); i++ {
newSampleEntry := SampleToChunkBoxEntry{
FirstChunk: binary.BigEndian.Uint32(content[position+(i*12) : position+4+(i*12)]),
SamplesPerChunk: binary.BigEndian.Uint32(content[position+4+(i*12) : position+8+(i*12)]),
SampleDescriptionIndex: binary.BigEndian.Uint32(content[position+8+(i*12) : position+12+(i*12)]),
}
box.Entries = append(box.Entries, newSampleEntry)
}
// validate entries amount
if len(box.Entries) != int(box.EntryCount) {
return box, fmt.Errorf("invalid amount of entries at %d; got %d but expected %d", filePosition, len(box.Entries), box.EntryCount)
}
return box, nil
}

60
TrackExtendsBox.go Normal file
View file

@ -0,0 +1,60 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
import (
"encoding/binary"
)
// TrackExtendsBox Track Extends Box struct
//
// 8.8.3 Track Extends Box
//
// This sets up default values used by the movie fragments. By setting defaults in this way, space and
// complexity can be saved in each Track Fragment Box.
type TrackExtendsBox struct {
*FullBox
// identifies the track; this shall be the track ID of a track in the Movie Box
TrackID uint32
DefaultSampleDescriptionIndex uint32
DefaultSampleDuration uint32
DefaultSampleSize uint32
DefaultSampleFlags uint32
}
const BoxTypeTrackExtends = "trex"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeTrackExtends,
ParentTypes: []string{BoxTypeMovieExtends},
Parser: ParseTrackExtendsBox,
})
}
// ParseTrackExtendsBox creates a new track extends box struct based on bytes
func ParseTrackExtendsBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &TrackExtendsBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
box.TrackID = binary.BigEndian.Uint32(content[4:8])
box.DefaultSampleDescriptionIndex = binary.BigEndian.Uint32(content[8:12])
box.DefaultSampleDuration = binary.BigEndian.Uint32(content[12:16])
box.DefaultSampleSize = binary.BigEndian.Uint32(content[16:20])
box.DefaultSampleFlags = binary.BigEndian.Uint32(content[20:24])
return box, nil
}

View file

@ -0,0 +1,80 @@
// Copyright 2024 Martin Riedl
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package gomp4
import "encoding/binary"
// TrackFragmentDecodeTimeBox Track Fragment Decode Time Box struct
//
// 8.8.12 Track fragment decode time
//
// Box Type: `tfdt
// Container: Track Fragment box (traf)
// Mandatory: No
// Quantity: Zero or one
//
// The Track Fragment Base Media Decode Time Box provides the absolute decode time, measured on the
// media timeline, of the first sample in decode order in the track fragment. This can be useful, for
// example, when performing random access in a file; it is not necessary to sum the sample durations of all
// preceding samples in previous fragments to find this value (where the sample durations are the deltas
// in the Decoding Time to Sample Box and the sample_durations in the preceding track runs).
//
// The Track Fragment Base Media Decode Time Box, if present, shall be positioned after the Track
// Fragment Header Box and before the first Track Fragment Run box.
//
// If the time expressed in the track fragment decode time (tfdt) box exceeds the sum of the durations of
// the samples in the preceding movie and movie fragments, then the duration of the last sample
// preceding this track fragment is extended such that the sum now equals the time given in this box. In
// this way, it is possible to generate a fragment containing a sample when the time of the next sample is
// not yet known.
//
// In particular, an empty track fragment (with no samples, but with a track fragment decode time box)
// may be used to establish the duration of the last sample.
type TrackFragmentDecodeTimeBox struct {
*FullBox
// is an integer equal to the sum of the decode durations of all earlier
// samples in the media, expressed in the media's timescale. It does not include the samples added
// in the enclosing track fragment.
BaseMediaDecodeTimeV0 uint32
// is an integer equal to the sum of the decode durations of all earlier
// samples in the media, expressed in the media's timescale. It does not include the samples added
// in the enclosing track fragment.
BaseMediaDecodeTimeV1 uint64
}
const BoxTypeTrackFragmentDecodeTime = "tfdt"
func init() {
BoxDefinitions = append(BoxDefinitions, BoxDefinition{
Type: BoxTypeTrackFragmentDecodeTime,
ParentTypes: []string{BoxTypeTrackFragment},
Parser: ParseTrackFragmentDecodeTimeBox,
})
}
// ParseTrackFragmentDecodeTimeBox creates a new track fragment decode time box struct based on bytes
func ParseTrackFragmentDecodeTimeBox(parser *Parser, filePosition uint64, headerSize uint32, content []byte) (any, error) {
box := &TrackFragmentDecodeTimeBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
if box.Version == 0 {
box.BaseMediaDecodeTimeV0 = binary.BigEndian.Uint32(content[4:8])
} else {
box.BaseMediaDecodeTimeV1 = binary.BigEndian.Uint64(content[4:12])
}
return box, nil
}

BIN
test/fmp4_init.mp4 Normal file

Binary file not shown.