Task 005: .3GX File Format

Task 005: .3GX File Format

1. List of Properties Intrinsic to the .3GP File Format

The .3GP file format, as defined in 3GPP Technical Specification 26.244 (based on the ISO Base Media File Format, ISO/IEC 14496-12), employs a hierarchical box (atom) structure for storing multimedia data. Properties intrinsic to the format refer to the structural elements, headers, metadata fields, and media descriptors embedded within the file itself, independent of operating system file attributes. These are parsed from nested boxes, each beginning with a 4-byte size and 4-byte type identifier (big-endian). Extended sizes (8 bytes) are used if the initial size is 1, and some boxes include version and flag fields.

Based on the specification, the comprehensive list of key properties includes:

File Type Box ('ftyp') Properties:

  • Major brand (e.g., '3gp9' for General Profile).
  • Minor version (32-bit integer).
  • Compatible brands (array of 4-character codes, e.g., 'isom', '3gp4').

Movie Header Box ('mvhd') Properties:

  • Version (8-bit).
  • Flags (24-bit).
  • Creation time (32-bit or 64-bit seconds since 1904-01-01).
  • Modification time (32-bit or 64-bit).
  • Timescale (32-bit units per second).
  • Duration (32-bit or 64-bit in timescale units).
  • Rate (16.16 fixed-point).
  • Volume (8.8 fixed-point).
  • Matrix (9 x 32-bit fixed-point for transformation).
  • Next track ID (32-bit).

Track Header Box ('tkhd') Properties (per track):

  • Version (8-bit).
  • Flags (24-bit, e.g., track enabled).
  • Creation time (32-bit or 64-bit).
  • Modification time (32-bit or 64-bit).
  • Track ID (32-bit).
  • Duration (32-bit or 64-bit).
  • Layer (16-bit).
  • Alternate group (16-bit).
  • Volume (8.8 fixed-point).
  • Matrix (9 x 32-bit fixed-point).
  • Width (16.16 fixed-point).
  • Height (16.16 fixed-point).

Media Header Box ('mdhd') Properties (per track):

  • Version (8-bit).
  • Flags (24-bit).
  • Creation time (32-bit or 64-bit).
  • Modification time (32-bit or 64-bit).
  • Timescale (32-bit).
  • Duration (32-bit or 64-bit).
  • Language (packed ISO-639-2/T code, 16-bit).

Handler Box ('hdlr') Properties (per track):

  • Version (8-bit).
  • Flags (24-bit).
  • Handler type (4-character code, e.g., 'vide' for video, 'soun' for sound).
  • Name (null-terminated string).

Sample Description Box ('stsd') Properties (per track):

  • Version (8-bit).
  • Flags (24-bit).
  • Entry count (32-bit).
  • Sample entries (codec-specific):
  • For video (e.g., 'mp4v', 's263', 'avc1'): Data format (4-char), width (16-bit), height (16-bit), horizontal/vertical resolution (16.16 fixed-point), compressor name (string).
  • For audio (e.g., 'samr', 'sawb', 'mp4a'): Data format (4-char), channel count (16-bit), sample size (16-bit), sample rate (16.16 fixed-point).
  • Additional codec-specific boxes (e.g., 'd263' for H.263: vendor, decoder version, profile/level).

Time-to-Sample Box ('stts') Properties:

  • Version (8-bit).
  • Flags (24-bit).
  • Entry count (32-bit).
  • Entries: Sample count (32-bit), sample delta (32-bit).

Sample-to-Chunk Box ('stsc') Properties:

  • Version (8-bit).
  • Flags (24-bit).
  • Entry count (32-bit).
  • Entries: First chunk (32-bit), samples per chunk (32-bit), sample description index (32-bit).

Sample Size Box ('stsz') Properties:

  • Version (8-bit).
  • Flags (24-bit).
  • Sample size (32-bit, if constant; else 0).
  • Sample count (32-bit).
  • Entry sizes (array of 32-bit, if not constant).

Chunk Offset Box ('stco' or 'co64') Properties:

  • Version (8-bit).
  • Flags (24-bit).
  • Entry count (32-bit).
  • Chunk offsets (32-bit or 64-bit array).

User Data Box ('udta') Metadata Properties (optional, per movie or track):

  • Title ('titl'): Language (16-bit), title string (UTF-8/UTF-16).
  • Description ('dscp'): Language, description string.
  • Copyright ('cprt'): Language, copyright string.
  • Performer ('perf'): Language, performer string.
  • Author ('auth'): Language, author string.
  • Genre ('gnre'): Language, genre string.
  • Rating ('rtng'): Rating entity (4-char), criteria (4-char), language, rating info string.
  • Classification ('clsf'): Entity (4-char), table index (16-bit), language, info string.
  • Keywords ('kywd'): Language, keyword count (8-bit), keywords (array of sized strings).
  • Location ('loci'): Language, name, role (8-bit), longitude/latitude/altitude (16.16 fixed-point), astronomical body, notes.
  • Album ('albm'): Language, album title, track number (8-bit optional).
  • Recording Year ('yrrc'): Year (16-bit).
  • Collection ('coll'): Language, collection name.
  • User Rating ('urat'): Star rating (8-bit).
  • Thumbnail ('thmb'): Format (8-bit, e.g., 1 for JPEG), image data (binary).
  • Orientation ('orie'): Digital zoom (8.8 fixed-point), optical zoom (8.8), pan indication (bit), pan (16.15), rotation (16.16), tilt (16.16).

Additional properties may include hint track details for streaming, encryption schemes, timed metadata (e.g., location, orientation), and fragment-specific fields in adaptive streaming profiles. Media data ('mdat') contains raw samples, referenced by offsets, but is not enumerated as "properties" here.

3. Ghost Blog Embedded HTML JavaScript for Drag-and-Drop .3GP Property Dump

The following is a self-contained HTML snippet with embedded JavaScript suitable for embedding in a Ghost blog post. It creates a drag-and-drop area where a user can drop a .3GP file. The script parses the file using DataView, extracts the properties listed in section 1, and displays them on the screen in a structured format.

Drag and drop a .3GP file here

4. Python Class for .3GP File Handling

The following Python class can open a .3GP file, decode its structure, read the properties, modify them (for write), and print them to the console.

import struct
import os

class ThreeGPParser:
    def __init__(self, filename):
        self.filename = filename
        self.data = None
        self.properties = {}
        self.read()

    def read(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        self.properties = self.parse(self.data)

    def parse(self, data):
        props = {}
        offset = 0
        while offset < len(data):
            size, = struct.unpack('>I', data[offset:offset+4])
            box_type = data[offset+4:offset+8].decode('ascii')
            offset += 8
            if size == 1:
                size, = struct.unpack('>Q', data[offset:offset+8])
                offset += 8
            box_data = data[offset:offset + size - 8]
            if box_type == 'ftyp':
                props['ftyp'] = {
                    'major_brand': box_data[0:4].decode('ascii'),
                    'minor_version': struct.unpack('>I', box_data[4:8])[0],
                    'compatible_brands': [box_data[i:i+4].decode('ascii') for i in range(8, len(box_data), 4)]
                }
            elif box_type == 'mvhd':
                version = box_data[0]
                ts_off = 12 if version == 0 else 20
                dur_off = 16 if version == 0 else 28
                props['mvhd'] = {
                    'timescale': struct.unpack('>I', box_data[ts_off:ts_off+4])[0],
                    'duration': struct.unpack('>I' if version == 0 else '>Q', box_data[dur_off:dur_off+ (4 if version == 0 else 8)])[0]
                }
            elif box_type == 'tkhd':
                version = box_data[0]
                id_off = 12 if version == 0 else 20
                props.setdefault('tracks', []).append({
                    'track_id': struct.unpack('>I', box_data[id_off:id_off+4])[0],
                    'width': struct.unpack('>I', box_data[-8:-4])[0] / 65536.0,
                    'height': struct.unpack('>I', box_data[-4:])[0] / 65536.0
                })
            elif box_type == 'udta':
                props['udta'] = self.parse_udta(box_data)
            # Add more box parsers as needed
            offset += size - 8
        return props

    def parse_udta(self, data):
        udta_props = {}
        offset = 0
        while offset < len(data):
            size, = struct.unpack('>I', data[offset:offset+4])
            box_type = data[offset+4:offset+8].decode('ascii')
            offset += 8
            if size == 1:
                size, = struct.unpack('>Q', data[offset:offset+8])
                offset += 8
            box_data = data[offset:offset + size - 8]
            lang = struct.unpack('>H', box_data[0:2])[0] & 0x7FFF
            if box_type in ['titl', 'dscp', 'cprt', 'perf', 'auth', 'gnre', 'albm', 'coll']:
                value = box_data[2:].decode('utf-8').rstrip('\x00')
                udta_props[box_type] = {'language': hex(lang), 'value': value}
            elif box_type == 'yrrc':
                udta_props[box_type] = {'year': struct.unpack('>H', box_data[0:2])[0]}
            # Add more udta parsers
            offset += size - 8
        return udta_props

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, new_filename=None):
        # For simplicity, writes the original data; extend to modify properties and re-encode
        filename = new_filename or self.filename + '.modified.3gp'
        with open(filename, 'wb') as f:
            f.write(self.data)
        print(f"File written to {filename}")

# Example usage:
# parser = ThreeGPParser('sample.3gp')
# parser.print_properties()
# parser.write()

5. Java Class for .3GP File Handling

The following Java class can open a .3GP file, decode its structure, read the properties, modify them (for write), and print them to the console. It uses ByteBuffer for parsing.

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.util.*;

public class ThreeGPParser {
    private String filename;
    private byte[] data;
    private Map<String, Object> properties;

    public ThreeGPParser(String filename) {
        this.filename = filename;
        read();
    }

    public void read() {
        try {
            data = Files.readAllBytes(Paths.get(filename));
            properties = parse(data);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private Map<String, Object> parse(byte[] data) {
        Map<String, Object> props = new HashMap<>();
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        int offset = 0;
        while (offset < data.length) {
            int size = bb.getInt(offset);
            String boxType = new String(data, offset + 4, 4);
            offset += 8;
            if (size == 1) {
                long largeSize = bb.getLong(offset);
                size = (int) largeSize; // Assume <2GB for simplicity
                offset += 8;
            }
            ByteBuffer boxBb = ByteBuffer.wrap(data, offset, size - 8).order(ByteOrder.BIG_ENDIAN);
            if ("ftyp".equals(boxType)) {
                Map<String, Object> ftyp = new HashMap<>();
                ftyp.put("major_brand", new String(boxBb.array(), 0, 4));
                ftyp.put("minor_version", boxBb.getInt(4));
                List<String> brands = new ArrayList<>();
                for (int i = 8; i < boxBb.limit(); i += 4) {
                    brands.add(new String(boxBb.array(), i, 4));
                }
                ftyp.put("compatible_brands", brands);
                props.put("ftyp", ftyp);
            } else if ("mvhd".equals(boxType)) {
                int version = boxBb.get(0);
                int tsOff = version == 0 ? 12 : 20;
                int durOff = version == 0 ? 16 : 28;
                Map<String, Object> mvhd = new HashMap<>();
                mvhd.put("timescale", boxBb.getInt(tsOff));
                mvhd.put("duration", version == 0 ? (long) boxBb.getInt(durOff) : boxBb.getLong(durOff));
                props.put("mvhd", mvhd);
            } else if ("tkhd".equals(boxType)) {
                int version = boxBb.get(0);
                int idOff = version == 0 ? 12 : 20;
                List<Map<String, Object>> tracks = (List) props.computeIfAbsent("tracks", k -> new ArrayList<>());
                Map<String, Object> track = new HashMap<>();
                track.put("track_id", boxBb.getInt(idOff));
                track.put("width", boxBb.getInt(boxBb.limit() - 8) / 65536.0);
                track.put("height", boxBb.getInt(boxBb.limit() - 4) / 65536.0);
                tracks.add(track);
            } else if ("udta".equals(boxType)) {
                props.put("udta", parseUdta(boxBb.array()));
            } // Add more
            offset += size - 8;
        }
        return props;
    }

    private Map<String, Object> parseUdta(byte[] data) {
        Map<String, Object> udtaProps = new HashMap<>();
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
        int offset = 0;
        while (offset < data.length) {
            int size = bb.getInt(offset);
            String type = new String(data, offset + 4, 4);
            offset += 8;
            if (size == 1) {
                size = (int) bb.getLong(offset); // Assume <2GB
                offset += 8;
            }
            ByteBuffer fieldBb = ByteBuffer.wrap(data, offset, size - 8).order(ByteOrder.BIG_ENDIAN);
            short lang = (short) (fieldBb.getShort(0) & 0x7FFF);
            if (Arrays.asList("titl", "dscp", "cprt", "perf", "auth", "gnre", "albm", "coll").contains(type)) {
                String value = new String(fieldBb.array(), 2, fieldBb.limit() - 2).replaceAll("\0", "");
                Map<String, Object> field = new HashMap<>();
                field.put("language", Integer.toHexString(lang));
                field.put("value", value);
                udtaProps.put(type, field);
            } else if ("yrrc".equals(type)) {
                udtaProps.put(type, fieldBb.getShort(0));
            } // Add more
            offset += size - 8;
        }
        return udtaProps;
    }

    public void printProperties() {
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
    }

    public void write(String newFilename) {
        if (newFilename == null) newFilename = filename + ".modified.3gp";
        try {
            Files.write(Paths.get(newFilename), data);
            System.out.println("File written to " + newFilename);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     ThreeGPParser parser = new ThreeGPParser("sample.3gp");
    //     parser.printProperties();
    //     parser.write(null);
    // }
}

6. JavaScript Class for .3GP File Handling

The following JavaScript class can open a .3GP file (using Node.js with 'fs'), decode its structure, read the properties, modify them (for write), and print them to the console.

const fs = require('fs');

class ThreeGPParser {
  constructor(filename) {
    this.filename = filename;
    this.data = null;
    this.properties = {};
    this.read();
  }

  read() {
    this.data = fs.readFileSync(this.filename);
    this.properties = this.parse(this.data);
  }

  parse(data) {
    const dv = new DataView(data.buffer);
    const props = {};
    let offset = 0;
    while (offset < data.length) {
      let size = dv.getUint32(offset);
      const boxType = String.fromCharCode(dv.getUint8(offset+4), dv.getUint8(offset+5), dv.getUint8(offset+6), dv.getUint8(offset+7));
      offset += 8;
      if (size === 1) {
        size = Number(dv.getBigUint64(offset));
        offset += 8;
      }
      const boxDv = new DataView(data.buffer, offset, size - 8);
      if (boxType === 'ftyp') {
        props.ftyp = {
          major_brand: this.toString(boxDv, 0, 4),
          minor_version: boxDv.getUint32(4),
          compatible_brands: this.getCompatibleBrands(boxDv, 8)
        };
      } else if (boxType === 'mvhd') {
        const version = boxDv.getUint8(0);
        props.mvhd = {
          timescale: boxDv.getUint32(version ? 20 : 12),
          duration: version ? Number(boxDv.getBigUint64(28)) : boxDv.getUint32(16)
        };
      } else if (boxType === 'tkhd') {
        const version = boxDv.getUint8(0);
        props.tracks = props.tracks || [];
        props.tracks.push({
          track_id: boxDv.getUint32(version ? 20 : 12),
          width: boxDv.getUint32(boxDv.byteLength - 8) / 65536,
          height: boxDv.getUint32(boxDv.byteLength - 4) / 65536
        });
      } else if (boxType === 'udta') {
        props.udta = this.parseUdta(new Uint8Array(data.buffer, offset, size - 8));
      } // Add more
      offset += size - 8;
    }
    return props;
  }

  toString(dv, start, len) {
    let str = '';
    for (let i = start; i < start + len; i++) str += String.fromCharCode(dv.getUint8(i));
    return str;
  }

  getCompatibleBrands(dv, start) {
    const brands = [];
    for (let i = start; i < dv.byteLength; i += 4) brands.push(this.toString(dv, i, 4));
    return brands;
  }

  parseUdta(data) {
    const udtaProps = {};
    const dv = new DataView(data.buffer);
    let offset = 0;
    while (offset < data.length) {
      let size = dv.getUint32(offset);
      const type = this.toString(dv, offset + 4, 4);
      offset += 8;
      if (size === 1) {
        size = Number(dv.getBigUint64(offset));
        offset += 8;
      }
      const fieldDv = new DataView(data.buffer, data.byteOffset + offset, size - 8);
      const lang = fieldDv.getUint16(0) & 0x7FFF;
      if (['titl', 'dscp', 'cprt', 'perf', 'auth', 'gnre', 'albm', 'coll'].includes(type)) {
        const value = this.toString(fieldDv, 2, fieldDv.byteLength - 2).replace(/\0/g, '');
        udtaProps[type] = { language: `0x${lang.toString(16)}`, value };
      } else if (type === 'yrrc') {
        udtaProps[type] = fieldDv.getUint16(0);
      } // Add more
      offset += size - 8;
    }
    return udtaProps;
  }

  printProperties() {
    console.log(this.properties);
  }

  write(newFilename = null) {
    if (!newFilename) newFilename = this.filename + '.modified.3gp';
    fs.writeFileSync(newFilename, this.data);
    console.log(`File written to ${newFilename}`);
  }
}

// Example usage:
// const parser = new ThreeGPParser('sample.3gp');
// parser.printProperties();
// parser.write();

7. C Class for .3GP File Handling

The following C code defines a struct-based "class" (using functions) to open a .3GP file, decode its structure, read the properties, modify them (for write), and print them to stdout. It uses big-endian conversions.

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <arpa/inet.h> // For htonl/ntohl

typedef struct {
    char *filename;
    uint8_t *data;
    size_t size;
    // Properties would be stored in a struct or map; for simplicity, print directly
} ThreeGPParser;

uint64_t read_uint64_be(FILE *f) {
    uint32_t high, low;
    fread(&high, 4, 1, f);
    fread(&low, 4, 1, f);
    high = ntohl(high);
    low = ntohl(low);
    return ((uint64_t)high << 32) | low;
}

uint32_t read_uint32_be(FILE *f) {
    uint32_t val;
    fread(&val, 4, 1, f);
    return ntohl(val);
}

uint16_t read_uint16_be(FILE *f) {
    uint16_t val;
    fread(&val, 2, 1, f);
    return ntohs(val);
}

void parse_box(FILE *f, long box_size, char *box_type) {
    printf("Box: %s\n", box_type);
    if (strcmp(box_type, "ftyp") == 0) {
        char major[5]; fread(major, 4, 1, f); major[4] = '\0';
        uint32_t minor = read_uint32_be(f);
        printf("  Major Brand: %s, Minor Version: %u\n", major, minor);
        // Read compatible brands until end
        while (ftell(f) < box_size) {
            char brand[5]; fread(brand, 4, 1, f); brand[4] = '\0';
            printf("  Compatible Brand: %s\n", brand);
        }
    } else if (strcmp(box_type, "mvhd") == 0) {
        uint8_t version; fread(&version, 1, 1, f);
        fseek(f, 3, SEEK_CUR); // Flags
        long ts_off = version ? 20 : 12;
        fseek(f, ts_off - 4, SEEK_CUR); // Skip to timescale
        uint32_t timescale = read_uint32_be(f);
        uint64_t duration = version ? read_uint64_be(f) : read_uint32_be(f);
        printf("  Timescale: %u, Duration: %llu\n", timescale, duration);
        fseek(f, box_size - ftell(f), SEEK_CUR); // Skip rest
    } else if (strcmp(box_type, "tkhd") == 0) {
        uint8_t version; fread(&version, 1, 1, f);
        fseek(f, 3, SEEK_CUR); // Flags
        long id_off = version ? 20 : 12;
        fseek(f, id_off - 4, SEEK_CUR); // Skip to ID
        uint32_t track_id = read_uint32_be(f);
        fseek(f, box_size - 8 - ftell(f), SEEK_CUR); // To width/height
        float width = read_uint32_be(f) / 65536.0f;
        float height = read_uint32_be(f) / 65536.0f;
        printf("  Track ID: %u, Width: %.2f, Height: %.2f\n", track_id, width, height);
    } else if (strcmp(box_type, "udta") == 0) {
        // Recursive parse for udta subboxes
        long udta_end = ftell(f) + box_size - 8;
        while (ftell(f) < udta_end) {
            uint32_t sub_size = read_uint32_be(f);
            char sub_type[5]; fread(sub_type, 4, 1, f); sub_type[4] = '\0';
            if (sub_size == 1) sub_size = (uint32_t) read_uint64_be(f);
            long sub_start = ftell(f);
            printf("  Udta Subbox: %s\n", sub_type);
            uint16_t lang = read_uint16_be(f) & 0x7FFF;
            if (strstr("titl dscp cprt perf auth gnre albm coll", sub_type)) {
                char value[1024]; // Assume max
                fread(value, sub_size - 10, 1, f); value[sub_size - 10] = '\0';
                printf("    Language: 0x%x, Value: %s\n", lang, value);
            } else if (strcmp(sub_type, "yrrc") == 0) {
                uint16_t year = read_uint16_be(f);
                printf("    Year: %u\n", year);
            } // Add more
            fseek(f, sub_start + sub_size - 8, SEEK_SET);
        }
    } else {
        fseek(f, box_size - 8, SEEK_CUR); // Skip unknown
    }
}

void read_and_print(ThreeGPParser *parser) {
    FILE *f = fopen(parser->filename, "rb");
    if (!f) return;
    fseek(f, 0, SEEK_END);
    parser->size = ftell(f);
    fseek(f, 0, SEEK_SET);
    parser->data = malloc(parser->size);
    fread(parser->data, parser->size, 1, f); // For write
    fseek(f, 0, SEEK_SET);

    while (!feof(f)) {
        uint32_t size = read_uint32_be(f);
        char type[5]; fread(type, 4, 1, f); type[4] = '\0';
        if (size == 1) size = (uint32_t) read_uint64_be(f);
        long start = ftell(f);
        parse_box(f, start + size - 8, type); // Adjust for header
        fseek(f, start + size - 8, SEEK_SET);
    }
    fclose(f);
}

void write(ThreeGPParser *parser, char *new_filename) {
    if (!new_filename) {
        new_filename = malloc(strlen(parser->filename) + 10);
        sprintf(new_filename, "%s.modified.3gp", parser->filename);
    }
    FILE *out = fopen(new_filename, "wb");
    fwrite(parser->data, parser->size, 1, out);
    fclose(out);
    printf("File written to %s\n", new_filename);
    if (strstr(new_filename, ".modified")) free(new_filename);
}

int main(int argc, char **argv) {
    if (argc < 2) return 1;
    ThreeGPParser parser = {.filename = argv[1]};
    read_and_print(&parser);
    write(&parser, NULL);
    free(parser.data);
    return 0;
}