initial check in

This commit is contained in:
Martin Riedl 2024-10-12 15:38:41 +02:00
commit 2e4a8fbcd6
Signed by: martinr92
GPG key ID: FB68DA65516A804C
20 changed files with 1304 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.DS_Store
.idea

102
.gitlab-ci.yml Normal file
View file

@ -0,0 +1,102 @@
image: golang:1.23
stages:
- test
- sonarcloud
- build
checks:
stage: test
script:
- go fmt $(go list ./...)
- go vet $(go list ./...)
code coverage:
stage: test
script:
- go test -covermode=count -coverprofile coverage.cov $(go list ./...)
- go tool cover -func=coverage.cov
- go tool cover -html=coverage.cov -o coverage.html
coverage: '/\(statements\)\W+\d+\.\d+%/'
artifacts:
paths:
- coverage.cov
- coverage.html
codecov.io:
stage: test
script:
- curl https://keybase.io/codecovsecurity/pgp_keys.asc | gpg --import
- curl -Os https://uploader.codecov.io/latest/linux/codecov
- curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM
- curl -Os https://uploader.codecov.io/latest/linux/codecov.SHA256SUM.sig
- gpg --verify codecov.SHA256SUM.sig codecov.SHA256SUM
- shasum -a 256 -c codecov.SHA256SUM
- chmod +x codecov
- go test -race -coverprofile=coverage.out -covermode=atomic
- ./codecov -t ${CODECOV_TOKEN}
rules:
- if: $CODECOV_TOKEN
when: on_success
# stage "sonarcloud" is only needed because of this issue:
# https://gitlab.com/gitlab-org/gitlab/-/issues/30632
sonarcloud-check:
stage: sonarcloud
# result of coverage is needed
needs:
- code coverage
image:
name: sonarsource/sonar-scanner-cli:latest
entrypoint: [""]
variables:
SONAR_USER_HOME: "${CI_PROJECT_DIR}/.sonar" # Defines the location of the analysis task cache
GIT_DEPTH: "0" # Tells git to fetch all the branches of the project, required by the analysis task
cache:
key: "${CI_JOB_NAME}"
paths:
- .sonar/cache
script:
- sonar-scanner
rules:
- if: $SONAR_HOST_URL
when: on_success
# build template
# execute the following command for all os/arch combinations: go tool dist list
.compile:
stage: build
# no dependencies -> no download of artifacts from previous jobs/stages
dependencies: []
script:
- go build .
darwin-amd64:
extends: .compile
variables:
GOOS: "darwin"
GOARCH: "amd64"
darwin-arm64:
extends: .compile
variables:
GOOS: "darwin"
GOARCH: "arm64"
linux-amd64:
extends: .compile
variables:
GOOS: "linux"
GOARCH: "amd64"
linux-arm64:
extends: .compile
variables:
GOOS: "linux"
GOARCH: "arm64"
windows-amd64:
extends: .compile
variables:
GOOS: "windows"
GOARCH: "amd64"

67
Box.go Normal file
View file

@ -0,0 +1,67 @@
// 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"
const (
boxSizeLength uint32 = 4
boxTypeLength uint32 = 4
// BoxTypeFileType File Type Box
BoxTypeFileType = "ftyp"
// BoxTypeMediaData Media Data Box
BoxTypeMediaData = "mdat"
// BoxTypeMovie Movie Box
BoxTypeMovie = "moov"
// BoxTypeMovieFragment Movie Fragment Box
BoxTypeMovieFragment = "moof"
// BoxTypeMovieFragmentHeader Movie Fragment Header Box
BoxTypeMovieFragmentHeader = "mfhd"
// BoxTypeTrackFragment Track Fragment Box
BoxTypeTrackFragment = "traf"
// BoxTypeTrackFragmentHeader Track Fragment Header
BoxTypeTrackFragmentHeader = "tfhd"
// BoxTypeTrackFragmentRun Track Fragment Run Box
BoxTypeTrackFragmentRun = "trun"
)
// Box is an abstract box struct
type Box struct {
FilePosition uint64
HeaderSize uint32
}
// FullBox is an abstract box type extending base box
type FullBox struct {
*Box
Version uint8
Flag []byte
flagUInt32 uint32
}
// newFullBox parses based on 4 bytes data the version number (first byte) and the flags (bytes 2 - 4)
func newFullBox(box *Box, data []byte) *FullBox {
fullBox := &FullBox{Box: box, Version: data[0], Flag: data[1:4]}
// build uint32 for flags (used for easier matching)
flag4Bytes := append(make([]byte, 1), fullBox.Flag...)
fullBox.flagUInt32 = binary.BigEndian.Uint32(flag4Bytes)
// extend header size
fullBox.HeaderSize = fullBox.HeaderSize + 4
return fullBox
}

87
FileTypeBox.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"
// FileTypeBox file type box struct
//
// 4.3 File Type Box
// Box Type: `ftyp
// Container: File
// Mandatory: Yes
// Quantity: Exactly one (but see below)
//
// Files written to this version of this specification must contain a filetype box. For compatibility with an
// earlier version of this specification, files may be conformant to this specification and not contain a file
// type box. Files with no filetype box should be read as if they contained an FTYP box with
// Major_brand='mp41', minor_version=0, and the single compatible brand 'mp41'.
//
// A mediafile structured to this part of this specification may be compatible with more than one detailed
// specification, and it is therefore not always possible to speak of a single type or brand for the file. This
// means that the utility of the file name extension and Multipurpose Internet Mail Extension (MIME) type
// are somewhat reduced.
//
// This box must be placed as early as possible in the file (e.g. after any obligatory signature, but before
// any significant variablesize boxes such as a Movie Box, Media Data Box, or Free Space). It identifies
// which specification is the best use of the file, and a minor version of that specification; and also a set of
// other specifications to which the file complies. Readers implementing this format should attempt to
// read files that are marked as compatible with any of the specifications that the reader implements. Any
// incompatible change in a specification should therefore register a new brand identifier to identify files
// conformant to the new specification.
//
// The minor version is informative only. It does not appear for compatiblebrands, and must not be used
// to determine the conformance of a file to a standard. It may allow more precise identification of the
// major specification, for inspection, debugging, or improved decoding.
//
// Files would normally be externally identified (e.g. with a file extension or mime type) that identifies the
// best use (major brand), or the brand that the author believes will provide the greatest compatibility.
//
// This section of this specification does not define any brands. However, see subclause 6.3 below for brands for
// files conformant to the whole specification and not just this section. All file format brands
// defined in this specification are included in Annex E with a summary of which features they require.
type FileTypeBox struct {
*Box
// is a brand identifier
MajorBrand string
// is an informative integer for the minor version of the major brand
MinorBrand uint32
// is a list, to the end of the box, of brands
CompatibleBrands []string
}
// ParseFileTypeBox creates new file type box based on bytes
func ParseFileTypeBox(filePosition uint64, headerSize uint32, content []byte) *FileTypeBox {
// create new box
box := &FileTypeBox{
Box: &Box{filePosition, headerSize},
}
// parse major brand
majorBrandBytes := content[0:4]
box.MajorBrand = string(majorBrandBytes)
// parse minor brand
box.MinorBrand = binary.BigEndian.Uint32(content[4:8])
// parse brands
for i := 8; i < len(content); i = i + 4 {
data := content[i : i+4]
brand := string(data)
box.CompatibleBrands = append(box.CompatibleBrands, brand)
}
return box
}

176
LICENSE.txt Normal file
View file

@ -0,0 +1,176 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

26
Log.go Normal file
View file

@ -0,0 +1,26 @@
// 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 (
"io/ioutil"
"log"
"os"
)
var (
logger = log.New(ioutil.Discard, "", 0)
verboseLogger = log.New(os.Stderr, "", log.Ldate|log.Ltime|log.Lshortfile)
)

52
MediaDataBox.go Normal file
View file

@ -0,0 +1,52 @@
// 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
// MediaDataBox Media Data Box
//
// 8.1.1 Media Data Box
// Box Type: mdat
// Container: File
// Mandatory: No
// Quantity: Zero or more
//
// This box contains the media data. In video tracks, this box would contain video frames. A presentation
// may contain zero or more Media Data Boxes. The actual media data follows the type field; its structure
// is described by the metadata (see particularly the sample table, subclause 8.5, and the item location box,
// subclause 8.11.3).
//
// In large presentations, it may be desirable to have more data in this box than a 32bit size would permit.
// In this case, the large variant of the size field, above in subclause 4.2, is used.
//
// There may be any number of these boxes in the file (including zero, if all the media data is in other files).
// The metadata refers to media data by its absolute offset within the file (see subclause 8.7.5, the Chunk
// Offset Box); so Media Data Box headers and free space may easily be skipped, and files without any box
// structure may also be referenced and used.
type MediaDataBox struct {
*Box
ContentStartPosition uint64
ContentEndPosition uint64
}
// ParseMediaDataBox creates a new media data box struct
func ParseMediaDataBox(filePosition uint64, headerSize uint32, content []byte) *MediaDataBox {
box := &MediaDataBox{Box: &Box{filePosition, headerSize}}
// parse positions of content
box.ContentStartPosition = filePosition + uint64(headerSize)
box.ContentEndPosition = box.ContentStartPosition + uint64(len(content))
return box
}

40
MovieBox.go Normal file
View file

@ -0,0 +1,40 @@
// 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
// MovieBox movie box struct
//
// 8.2.1 Movie Box
// Box Type: moov
// Container: File
// Mandatory: Yes
// Quantity: Exactly one
//
// The metadata for a presentation is stored in the single Movie Box which occurs at the toplevel of a file.
// Normally this box is close to the beginning or end of the file, though this is not required.
type MovieBox struct {
*Box
ChildBoxes []interface{}
}
// ParseMovieBox creates a new movie box struct based on bytes
func ParseMovieBox(filePosition uint64, headerSize uint32, content []byte) (*MovieBox, error) {
box := &MovieBox{Box: &Box{filePosition, headerSize}}
// parse content boxes
var err error
box.ChildBoxes, err = box.parseChildBoxes(filePosition, content)
return box, err
}

51
MovieFragmentBox.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
// MovieFragmentBox Movie Fragment Box struct
//
// 8.8.4 Movie Fragment Box
// Box Type: moof
// Container: File
// Mandatory: No
// Quantity: Zero or more
//
// The movie fragments extend the presentation in time. They provide the information that would
// previously have been in the Movie Box. The actual samples are in Media Data Boxes, as usual, if they are
// in the same file. The data reference index is in the sample description, so it is possible to build
// incremental presentations where the media data is in files other than the file containing the Movie Box.
//
// The Movie Fragment Box is a toplevel box, (i.e. a peer to the Movie Box and Media Data boxes). It
// contains a Movie Fragment Header Box, and then one or more Track Fragment Boxes.
//
// NOTE There is no requirement that any particular movie fragment extend all tracks present in the movie
// header, and there is no restriction on the location of the media data referred to by the movie fragments.
// However, derived specifications may make such restrictions.
type MovieFragmentBox struct {
*Box
ChildBoxes []interface{}
}
// ParseMovieFragmentBox creates a new movie fragment box struct based on bytes
func ParseMovieFragmentBox(filePosition uint64, headerSize uint32, content []byte) (*MovieFragmentBox, error) {
box := &MovieFragmentBox{
Box: &Box{filePosition, headerSize},
}
// parse child boxes
var err error
box.ChildBoxes, err = box.parseChildBoxes(filePosition, content)
return box, err
}

49
MovieFragmentHeaderBox.go Normal file
View file

@ -0,0 +1,49 @@
// 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"
)
// MovieFragmentHeaderBox Movie Fragment Header Box struct
//
// 8.8.5 Movie Fragment Header Box
// Box Type: mfhd
// Container: Movie Fragment Box ('moof')
// Mandatory: Yes
// Quantity: Exactly one
//
// The movie fragment header contains a sequence number, as a safety check. The sequence number
// usually starts at 1 and increases for each movie fragment in the file, in the order in which they occur.
// This allows readers to verify integrity of the sequence in environments where undesired reordering
// might occur.
type MovieFragmentHeaderBox struct {
*FullBox
// a number associated with this fragment
SequenceNumber uint32
}
// ParseMovieFragmentHeaderBox creates a new Movie Fragment Header Box struct
func ParseMovieFragmentHeaderBox(filePosition uint64, headerSize uint32, content []byte) *MovieFragmentHeaderBox {
box := &MovieFragmentHeaderBox{
FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4]),
}
// parse sequence number
box.SequenceNumber = binary.BigEndian.Uint32(content[4:8])
return box
}

160
Parser.go Normal file
View file

@ -0,0 +1,160 @@
// 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 (
"bytes"
"encoding/binary"
"errors"
"fmt"
"io"
)
// Parser struct for mp4 container parsing
type Parser struct {
reader io.Reader
Content []interface{}
}
// NewParser creates a new parser with byte data
func NewParser(reader io.Reader) *Parser {
return &Parser{reader: reader}
}
// Parse starts the file parsing
func (parser *Parser) Parse() error {
var filePosition uint64 = 0
for {
box, endPosition, endOfFile, err := parseNextBox(parser.reader, filePosition)
if box != nil {
parser.Content = append(parser.Content, box)
}
if endOfFile {
return nil
} else if err != nil {
logger.Println("error at", endPosition)
return err
}
filePosition = endPosition
}
}
func parseNextBox(reader io.Reader, filePosition uint64) (box interface{}, endPosition uint64, endOfFile bool, err error) {
// first 4 bytes are the size of the box
sizeBytes := make([]byte, boxSizeLength)
sizeBytesCount, err := reader.Read(sizeBytes)
if err != nil {
if err == io.EOF {
return nil, filePosition, true, nil
}
return nil, filePosition, false, err
}
// check read bytes count
if sizeBytesCount != len(sizeBytes) {
return nil, filePosition, false, fmt.Errorf("unable to parse next box size")
}
// parse box size
boxSize := binary.BigEndian.Uint32(sizeBytes)
logger.Println("new box size", boxSize, "at", filePosition)
// TODO: check large size box type
// parse box type
boxTypeBytes := make([]byte, boxTypeLength)
boxTypeBytesCount, err := reader.Read(boxTypeBytes)
if err != nil {
return nil, filePosition, false, err
}
// check read bytes count
if boxTypeBytesCount != len(boxTypeBytes) {
return nil, filePosition, false, fmt.Errorf("unable to parse box type at %d", filePosition)
}
boxType := string(boxTypeBytes)
logger.Println("new box type", boxType, "at", filePosition)
// parse box data
boxHeaderSize := boxSizeLength + boxTypeLength
boxContentSize := boxSize - boxHeaderSize
boxContentBytes := make([]byte, boxContentSize)
boxContentBytesCount, err := reader.Read(boxContentBytes)
if err != nil {
return nil, filePosition, false, err
}
// check read bytes count
if boxContentBytesCount != len(boxContentBytes) {
return nil, filePosition, false, fmt.Errorf("unable to parse box content at %d", filePosition)
}
// parse struct of box type
switch boxType {
case BoxTypeFileType:
box = ParseFileTypeBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeMediaData:
box = ParseMediaDataBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeMovie:
box, err = ParseMovieBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeMovieFragment:
box, err = ParseMovieFragmentBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeMovieFragmentHeader:
box = ParseMovieFragmentHeaderBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeTrackFragment:
box, err = ParseTrackFragmentBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeTrackFragmentHeader:
box = ParseTrackFragmentHeaderBox(filePosition, boxHeaderSize, boxContentBytes)
case BoxTypeTrackFragmentRun:
box, err = ParseTrackFragmentRunBox(filePosition, boxHeaderSize, boxContentBytes)
default:
logger.Println("unknown box type", boxType, "at", filePosition)
err = errors.New("unknown box type " + boxType)
}
// check for box errors
if err != nil {
return
}
// calculate end position
endPosition = filePosition + uint64(boxSize)
return
}
func (box *Box) parseChildBoxes(filePosition uint64, content []byte) (subBoxes []interface{}, e error) {
byteReader := bytes.NewReader(content)
endPosition := filePosition + uint64(box.HeaderSize) + uint64(len(content))
var currentFilePosition uint64 = filePosition + uint64(box.HeaderSize)
for {
// parse next packet
currentBox, currentEndPosition, _, err := parseNextBox(byteReader, currentFilePosition)
if err != nil {
e = err
logger.Println("error at", currentEndPosition, err)
return
}
currentFilePosition = currentEndPosition
// store packet
subBoxes = append(subBoxes, currentBox)
// check for end of content
if endPosition == currentEndPosition {
return
}
}
}

83
Parser_test.go Normal file
View file

@ -0,0 +1,83 @@
// 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 (
"os"
"testing"
)
func TestMain(m *testing.M) {
// enable debug log
logger = verboseLogger
// run test
code := m.Run()
os.Exit(code)
}
func TestParser(t *testing.T) {
parser, err := testFile(t, "test/fmp4.mp4")
if err != nil {
t.Error(err)
}
// check box count
if len(parser.Content) != 10 {
t.Error("invalid amount of boxes", len(parser.Content))
}
// validate file type data
validateFileType(t, parser)
// TODO: validate movie fragment header data
}
func validateFileType(t *testing.T, parser *Parser) {
// get first file type box
fileTypeBox := parser.Content[0].(*FileTypeBox)
// check major brand
if fileTypeBox.MajorBrand != "mp42" {
t.Error("invalid major brand", fileTypeBox.MajorBrand)
}
// check minor brand
if fileTypeBox.MinorBrand != 0x00000001 {
t.Error("invalid minor brand", fileTypeBox.MinorBrand)
}
// TODO: check
}
func TestParserInvalidSampleCount(t *testing.T) {
if _, err := testFile(t, "test/fmp4-incorrect-sample-count.mp4"); err == nil {
t.Error("missing error of invalid sample count of track fragment run box")
}
}
func testFile(t *testing.T, filePath string) (*Parser, error) {
// open test file
file, err := os.Open(filePath)
if err != nil {
t.Error(err)
}
defer file.Close()
// create new parser
parser := NewParser(file)
err = parser.Parse()
return parser, err
}

111
README.md Normal file
View file

@ -0,0 +1,111 @@
# goMP4
[![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
## Parser
```go
parser := gomp4.NewParser(file)
if err := parser.Parse(); err != nil {
panic(err)
}
```
## Progress
Implementation progress
| Chapter | Box Types | Parser |
|----------------------------------------------------|----------------|-------:|
| 4.3 File Type Box | ftyp | 100% |
| 8.1.1 Media Data Box | mdat | 100% |
| 8.1.2 Free Space Box | free, skip | - |
| 8.1.3 Progressive Download Information Box | pdin | - |
| 8.2.1 Movie Box | moov | 100% |
| 8.2.2 Movie Header Box | mvhd | - |
| 8.3.1 Track Box | trak | - |
| 8.3.2 Track Header Box | tkhd | - |
| 8.3.3 Track Reference Box | tref | - |
| 8.3.4 Track Group Box | trgr | - |
| 8.4.1 Media Box | mdia | - |
| 8.4.2 Media Header Box | mdhd | - |
| 8.4.3 Handler Reference Box | hdlr | - |
| 8.4.4 Media Information Box | minf | - |
| 8.4.5.2 Null Media Header Box | nmhd | - |
| 8.4.6 Extended language tag | elng | - |
| 8.5.1 Sample Table Box | stbl | - |
| 8.5.2 Sample Description Box | stsd | - |
| 8.5.3 Degradation Priority Box | stdp | - |
| 8.6.1.2 Decoding Time to Sample Box | stts | - |
| 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 | - |
| 8.6.3 Shadow Sync Sample Box | stsh | - |
| 8.6.4 Independent and Disposable Samples Box | sdtp | - |
| 8.6.5 Edit Box | edts | - |
| 8.6.6 Edit List Box | elst | - |
| 8.7.1 Data Information Box | dinf | - |
| 8.7.2 Data Reference Box | dref, url, urn | - |
| 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.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.2 Movie Extends Header Box | mehd | - |
| 8.8.3 Track Extends Box | trex | - |
| 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% |
| 8.8.7 Track Fragment Header Box | tfhd | 20% |
| 8.8.8 Track Fragment Run Box | traf | 100% |
| 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.13 Level Assignment Box | leva | - |
| 8.8.15 Track Extension Properties Box | trep | - |
| 8.8.16 Alternative Startup Sequence Properties Box | assp | - |
| 8.9.2 Sample to Group Box | sbgp | - |
| 8.9.3 Sample Group Description Box | sgpd | - |
| 8.10.1 User Data Box | udta | - |
| 8.10.2 Copyright Box | cprt | - |
| 8.10.3 Track Selection Box | tsel | - |
| 8.10.4 Track kind | kind | - |
| 8.11.1 The Meta box | meta | - |
| 8.11.2 XML Boxes | xml, bxml | - |
| 8.11.3 The Item Location Box | iloc | - |
| 8.11.4 Primary Item Box | pitm | - |
| 8.11.5 Item Protection Box | ipro | - |
| 8.11.6 Item Information Box | iinf | - |
| 8.11.7 Additional Metadata Container Box | meco | - |
| 8.11.8 Metabox Relation Box | mere | - |
| 8.11.11 Item Data Box | idat | - |
| 8.11.12 Item Reference Box | iref | - |
| 8.12.1 Protection Scheme Information Box | sinf | - |
| 8.12.2 Original Format Box | frma | - |
| 8.12.5 Scheme Type Box | schm | - |
| 8.12.6 Scheme Information Box | schi | - |
| 8.13.2 FD Item Information Box | fiin | - |
| 8.13.3 File Partition Box | fpar | - |
| 8.13.4 FEC Reservoir Box | fecr | - |
| 8.13.5 FD Session Group Box | segr | - |
| 8.13.6 Group ID to Name Box | gitn | - |
| 8.13.7 File Reservoir Box | fire | - |
| 8.14.3 Sub Track box | strk | - |
| 8.14.4 Sub Track Information box | stri | - |
| 8.14.5 Sub Track Definition box | strd | - |
| 8.14.6 Sub Track Sample Group box | stsg | - |
| 8.15.3 Restricted Scheme Information box | rinf | - |
| 8.15.4.2 Stereo video box | stvi | - |
| 8.16.2 Segment Type Box | styp | - |
| 8.16.3 Segment Index Box | sidx | - |
| 8.16.4 Subsegment Index Box | ssix | - |
| 8.16.5 Producer Reference Time Box | prft | - |
| 8.17.3 Complete Track Information Box | cinf | - |
| 9.1.2.1 SRTP Process box | srpp | - |
## Helper Tools
http://mp4parser.com/

44
TrackFragmentBox.go Normal file
View file

@ -0,0 +1,44 @@
// 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
// TrackFragmentBox Track Fragment Box struct
//
// 8.8.6 Track Fragment Box
// Box Type: traf
// Container: Movie Fragment Box ('moof')
// Mandatory: No
// Quantity: Zero or more
//
// Within the movie fragment there is a set of track fragments, zero or more per track. The track fragments
// in turn contain zero or more track runs, each of which document a contiguous run of samples for that
// track. Within these structures, many fields are optional and can be defaulted.
//
// It is possible to add 'empty time' to a track using these structures, as well as adding samples. Empty
// inserts can be used in audio tracks doing silence suppression, for example.
type TrackFragmentBox struct {
*Box
ChildBoxes []interface{}
}
// ParseTrackFragmentBox creates a new Track Fragment Box struct
func ParseTrackFragmentBox(filePosition uint64, headerSize uint32, content []byte) (*TrackFragmentBox, error) {
box := &TrackFragmentBox{Box: &Box{filePosition, headerSize}}
// parse child boxes
var err error
box.ChildBoxes, err = box.parseChildBoxes(filePosition, content)
return box, err
}

88
TrackFragmentHeaderBox.go Normal file
View file

@ -0,0 +1,88 @@
// 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"
// TrackFragmentHeaderBox Track Fragment Header
//
// 8.8.7 Track Fragment Header Box
// Box Type: tfhd
// Container: Track Fragment Box ('traf')
// Mandatory: Yes
// Quantity: Exactly one
//
// Each movie fragment can add zero or more fragments to each track; and a track fragment can add zero
// or more contiguous runs of samples. The track fragment header sets up information and defaults used
// for those runs of samples.
//
// The basedataoffset, if explicitly provided, is a data offset that is identical to a chunk offset in the Chunk
// Offset Box, i.e. applying to the complete file (e.g. starting with a filetype box and movie box). In
// circumstances when the complete file does not exist or its size is unknown, it may be impossible to use
// an explicit basedataoffset; then, offsets need to be established relative to the movie fragment.
type TrackFragmentHeaderBox struct {
*FullBox
TrackID uint32
// the base offset to use when calculating data offsets
BaseDataOffset uint64
SampleDescriptionIndex uint32
DefaultSampleDuration uint32
DefaultSampleSize uint32
DefaultSampleFlags uint32
}
const (
// TrackFragmentHeaderBoxFlagBaseDataOffsetPresent Base Offset Present Flag
//
// indicates the presence of the basedataoffset field. This
// provides an explicit anchor for the data offsets in each track run (see below). If not provided and
// if the defaultbaseismoof flag is not set, the basedataoffset for the first track in the movie
// fragment is the position of the first byte of the enclosing Movie Fragment Box, and for second
// and subsequent track fragments, the default is the end of the data defined by the preceding
// track fragment. Fragments 'inheriting' their offset in this way must all use the same data
// reference (i.e., the data for these tracks must be in the same file)
TrackFragmentHeaderBoxFlagBaseDataOffsetPresent uint32 = 0x00000001
// TrackFragmentHeaderBoxFlagSampleDescriptionIndexPresent Sample Description Index Present Flag
//
// indicates the presence of this field, which overrides,
// in this fragment, the default set up in the Track Extends Box.
TrackFragmentHeaderBoxFlagSampleDescriptionIndexPresent uint32 = 0x00000002
// TrackFragmentHeaderBoxFlagDefaultSampleDurationPresent Default Sample Duration Present Flag
TrackFragmentHeaderBoxFlagDefaultSampleDurationPresent uint32 = 0x00000008
// TrackFragmentHeaderBoxFlagDefaultSampleSize Default Sample Size Present Flag
TrackFragmentHeaderBoxFlagDefaultSampleSize uint32 = 0x00000010
// TrackFragmentHeaderBoxFlagDefaultSampleFlags Default Sample Flags Present Flag
TrackFragmentHeaderBoxFlagDefaultSampleFlags uint32 = 0x00000020
// TODO: define other flags
)
// ParseTrackFragmentHeaderBox creates a new track fragment header box struct
func ParseTrackFragmentHeaderBox(filePosition uint64, headerSize uint32, content []byte) *TrackFragmentHeaderBox {
box := &TrackFragmentHeaderBox{FullBox: newFullBox(&Box{filePosition, headerSize}, content[0:4])}
position := 4
// parse track ID
box.TrackID = binary.BigEndian.Uint32(content[position : position+4])
position += 4
// TODO: check other flags
return box
}

163
TrackFragmentRunBox.go Normal file
View file

@ -0,0 +1,163 @@
// 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"
)
// TrackFragmentRunBox Track Fragment Run Box struct
//
// 8.8.8 Track Fragment Run Box
// Box Type: trun
// Container: Track Fragment Box ('traf')
// Mandatory: No
// Quantity: Zero or more
//
// Within the Track Fragment Box, there are zero or more Track Run Boxes. If the durationisempty flag is
// set in the tf_flags, there are no track runs. A track run documents a contiguous set of samples for a track.
//
// The number of optional fields is determined from the number of bits set in the lower byte of the flags,
// and the size of a record from the bits set in the second byte of the flags. This procedure shall be
// followed, to allow for new fields to be defined.
//
// If the dataoffset is not present, then the data for this run starts immediately after the data of the
// previous run, or at the basedataoffset defined by the track fragment header if this is the first run in a
// track fragment, If the dataoffset is present, it is relative to the basedataoffset established in the track
// fragment header.
//
// The composition offset values in the composition timetosample box and in the track run box may be
// signed or unsigned. The recommendations given in the composition timetosample box concerning the
// use of signed composition offsets also apply here.
type TrackFragmentRunBox struct {
*FullBox
// the number of samples being added in this run; also the number of rows in the
// following table (the rows can be empty)
SampleCount uint32
// is added to the implicit or explicit data_offset established in the track fragment header.
DataOffset int32
// provides a set of flags for the first sample only of this run.
FirstSampleFlags uint32
Samples []TrackFragmentRunBoxSample
}
const (
// TrackFragmentRunBoxFlagDataOffsetPresent Data Offset Flag
TrackFragmentRunBoxFlagDataOffsetPresent uint32 = 0x00000001
// TrackFragmentRunBoxFlagFirstSampleFlagsPresent First Sample Flags
// this overrides the default flags for the first sample only. This
// makes it possible to record a group of frames where the first is a key and the rest are difference
// frames, without supplying explicit flags for every sample. If this flag and field are used, sample
// flags shall not be present.
TrackFragmentRunBoxFlagFirstSampleFlagsPresent uint32 = 0x00000004
// TrackFragmentRunBoxFlagSampleDurationPresent Sample Duration Flag
// indicates that each sample has its own duration, otherwise the default is used.
TrackFragmentRunBoxFlagSampleDurationPresent uint32 = 0x00000100
// TrackFragmentRunBoxFlagSampleSizePresent Sample Size Flag
// each sample has its own size, otherwise the default is used.
TrackFragmentRunBoxFlagSampleSizePresent uint32 = 0x00000200
// TrackFragmentRunBoxFlagSampleFlagsPresent Sample Flags Present Flag
// each sample has its own flags, otherwise the default is used.
TrackFragmentRunBoxFlagSampleFlagsPresent uint32 = 0x00000400
// TrackFragmentRunBoxFlagSampleCompositionTimeOffsetPresent Sample Composition Time Offset
// each sample has a composition time offset
// (e.g. as used for I/P/B video in MPEG).
TrackFragmentRunBoxFlagSampleCompositionTimeOffsetPresent uint32 = 0x00000800
)
// ParseTrackFragmentRunBox creates a new Track Fragment Run Box struct
func ParseTrackFragmentRunBox(filePosition uint64, headerSize uint32, content []byte) (*TrackFragmentRunBox, error) {
// build full box with additional 4 bytes header
fullBox := newFullBox(&Box{filePosition, headerSize}, content[0:4])
position := 4
// create track fragment run box
box := &TrackFragmentRunBox{FullBox: fullBox}
// parse sample counter
box.SampleCount = binary.BigEndian.Uint32(content[position : position+4])
position += 4
// parse data offset
if fullBox.flagUInt32&TrackFragmentRunBoxFlagDataOffsetPresent == TrackFragmentRunBoxFlagDataOffsetPresent {
box.DataOffset = int32(binary.BigEndian.Uint32(content[position : position+4]))
position += 4
}
// parse first sample flags
if fullBox.flagUInt32&TrackFragmentRunBoxFlagFirstSampleFlagsPresent == TrackFragmentRunBoxFlagFirstSampleFlagsPresent {
box.FirstSampleFlags = binary.BigEndian.Uint32(content[position : position+4])
position += 4
}
// parse samples
for i := uint32(0); i < box.SampleCount; i++ {
sample := TrackFragmentRunBoxSample{}
// parse duration
if fullBox.flagUInt32&TrackFragmentRunBoxFlagSampleDurationPresent == TrackFragmentRunBoxFlagSampleDurationPresent {
sample.Duration = binary.BigEndian.Uint32(content[position : position+4])
position += 4
}
// parse size
if fullBox.flagUInt32&TrackFragmentRunBoxFlagSampleSizePresent == TrackFragmentRunBoxFlagSampleSizePresent {
sample.Size = binary.BigEndian.Uint32(content[position : position+4])
position += 4
}
// parse flags
if fullBox.flagUInt32&TrackFragmentRunBoxFlagSampleFlagsPresent == TrackFragmentRunBoxFlagSampleFlagsPresent {
sample.Flags = binary.BigEndian.Uint32(content[position : position+4])
position += 4
}
// parse composition time offset
if fullBox.flagUInt32&TrackFragmentRunBoxFlagSampleCompositionTimeOffsetPresent == TrackFragmentRunBoxFlagSampleCompositionTimeOffsetPresent {
if fullBox.Version == 0 {
sample.CompositionTimeOffsetV0 = binary.BigEndian.Uint32(content[position : position+4])
position += 4
} else {
sample.CompositionTimeOffset = int32(binary.BigEndian.Uint32(content[position : position+4]))
position += 4
}
}
// store new sample
box.Samples = append(box.Samples, sample)
}
// check, if everything has been parsed
if position != len(content) {
return nil, fmt.Errorf("invalid byte position %d; expected position %d in file box position %d", position, len(content), filePosition)
}
return box, nil
}
// TrackFragmentRunBoxSample contains sample information
type TrackFragmentRunBoxSample struct {
Duration uint32
Size uint32
Flags uint32
CompositionTimeOffsetV0 uint32
CompositionTimeOffset int32
}

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module gitlab.com/martinr92/gomp4
go 1.15

Binary file not shown.

Binary file not shown.

BIN
test/fmp4.mp4 Normal file

Binary file not shown.