commit 2e4a8fbcd6cbddb8ee1ec610c9c680400282629f Author: Martin Riedl Date: Sat Oct 12 15:38:41 2024 +0200 initial check in diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4befed3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.DS_Store +.idea diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000..0facea4 --- /dev/null +++ b/.gitlab-ci.yml @@ -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" diff --git a/Box.go b/Box.go new file mode 100644 index 0000000..3bb6bb0 --- /dev/null +++ b/Box.go @@ -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 +} diff --git a/FileTypeBox.go b/FileTypeBox.go new file mode 100644 index 0000000..5a26c57 --- /dev/null +++ b/FileTypeBox.go @@ -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 file‐type 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 file‐type 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 media‐file 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 variable‐size 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 compatible‐brands, 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 +} diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE.txt @@ -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 diff --git a/Log.go b/Log.go new file mode 100644 index 0000000..b0beab3 --- /dev/null +++ b/Log.go @@ -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) +) diff --git a/MediaDataBox.go b/MediaDataBox.go new file mode 100644 index 0000000..94d0194 --- /dev/null +++ b/MediaDataBox.go @@ -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 32‐bit 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 +} diff --git a/MovieBox.go b/MovieBox.go new file mode 100644 index 0000000..6a305de --- /dev/null +++ b/MovieBox.go @@ -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 top‐level 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 +} diff --git a/MovieFragmentBox.go b/MovieFragmentBox.go new file mode 100644 index 0000000..d86925c --- /dev/null +++ b/MovieFragmentBox.go @@ -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 top‐level 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 +} diff --git a/MovieFragmentHeaderBox.go b/MovieFragmentHeaderBox.go new file mode 100644 index 0000000..ef978a6 --- /dev/null +++ b/MovieFragmentHeaderBox.go @@ -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 re‐ordering +// 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 +} diff --git a/Parser.go b/Parser.go new file mode 100644 index 0000000..be94b53 --- /dev/null +++ b/Parser.go @@ -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 + } + } +} diff --git a/Parser_test.go b/Parser_test.go new file mode 100644 index 0000000..bd5f1fb --- /dev/null +++ b/Parser_test.go @@ -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 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d909a72 --- /dev/null +++ b/README.md @@ -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/ diff --git a/TrackFragmentBox.go b/TrackFragmentBox.go new file mode 100644 index 0000000..1b661f6 --- /dev/null +++ b/TrackFragmentBox.go @@ -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 +} diff --git a/TrackFragmentHeaderBox.go b/TrackFragmentHeaderBox.go new file mode 100644 index 0000000..b228322 --- /dev/null +++ b/TrackFragmentHeaderBox.go @@ -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 base‐data‐offset, 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 file‐type 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 base‐data‐offset; 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 base‐data‐offset field. This + // provides an explicit anchor for the data offsets in each track run (see below). If not provided and + // if the default‐base‐is‐moof flag is not set, the base‐data‐offset 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 over‐rides, + // 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 +} diff --git a/TrackFragmentRunBox.go b/TrackFragmentRunBox.go new file mode 100644 index 0000000..aa95831 --- /dev/null +++ b/TrackFragmentRunBox.go @@ -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 duration‐is‐empty 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 data‐offset is not present, then the data for this run starts immediately after the data of the +// previous run, or at the base‐data‐offset defined by the track fragment header if this is the first run in a +// track fragment, If the data‐offset is present, it is relative to the base‐data‐offset established in the track +// fragment header. +// +// The composition offset values in the composition time‐to‐sample box and in the track run box may be +// signed or unsigned. The recommendations given in the composition time‐to‐sample 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 over‐rides 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 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..f4cba21 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module gitlab.com/martinr92/gomp4 + +go 1.15 diff --git a/specification/ISO_IEC_14496-12_2015.pdf b/specification/ISO_IEC_14496-12_2015.pdf new file mode 100644 index 0000000..127663c Binary files /dev/null and b/specification/ISO_IEC_14496-12_2015.pdf differ diff --git a/test/fmp4-incorrect-sample-count.mp4 b/test/fmp4-incorrect-sample-count.mp4 new file mode 100644 index 0000000..119eece Binary files /dev/null and b/test/fmp4-incorrect-sample-count.mp4 differ diff --git a/test/fmp4.mp4 b/test/fmp4.mp4 new file mode 100644 index 0000000..4155bce Binary files /dev/null and b/test/fmp4.mp4 differ