Compare commits
10 commits
0eedf34735
...
d88090f541
Author | SHA1 | Date | |
---|---|---|---|
d88090f541 | |||
c96343ad39 | |||
f2a9a75725 | |||
503c5657ef | |||
ca8bedd8d3 | |||
29e84abf5b | |||
27e34124ed | |||
94aed731f2 | |||
309ecb9a79 | |||
1c406419a5 |
14 changed files with 718 additions and 20 deletions
51
AVCVideoStream.go
Normal file
51
AVCVideoStream.go
Normal 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
87
ChunkOffsetBox.go
Normal 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 32‐bit or 64‐bit 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 self‐contained 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
|
||||
}
|
95
DecodingTimeToSampleBox.go
Normal file
95
DecodingTimeToSampleBox.go
Normal 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 time‐to‐sample 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 non‐negative.
|
||||
//
|
||||
// The DT axis has a zero origin; DT(i) = SUM(for j=0 to i‐1 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 non‐empty (non‐zero).
|
||||
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 time‐scale 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
|
||||
}
|
6
Fixed.go
6
Fixed.go
|
@ -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
55
MovieExtendsBox.go
Normal 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
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
24
README.md
24
README.md
|
@ -3,7 +3,7 @@
|
|||
[](https://gitlab.com/martinr92/gomp4/commits/main)
|
||||
[](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/)
|
||||
|
|
|
@ -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 pixels‐per‐inch, as a fixed 16.16 number
|
||||
HorizResolution Fixed1616
|
||||
// give the resolution of the image in pixels‐per‐inch, 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 32‐byte 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
87
SampleSizeBox.go
Normal 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 32‐bit 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 sample‐size 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
97
SampleToChunkBox.go
Normal 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
|
||||
// samples‐per‐chunk.
|
||||
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 samples‐per‐chunk and sample‐description‐index; 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
60
TrackExtendsBox.go
Normal 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
|
||||
}
|
80
TrackFragmentDecodeTimeBox.go
Normal file
80
TrackFragmentDecodeTimeBox.go
Normal 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
BIN
test/fmp4_init.mp4
Normal file
Binary file not shown.
Loading…
Add table
Reference in a new issue