2026-07-13 17:47:00 +07:00
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"bufio"
|
|
|
|
|
|
"encoding/binary"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"math"
|
|
|
|
|
|
"os"
|
2026-07-16 21:56:41 +07:00
|
|
|
|
"sync"
|
2026-07-13 17:47:00 +07:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// .tsf container, little-endian: "TSFR" | version uint16 | fps float64 |
|
2026-07-13 22:54:23 +07:00
|
|
|
|
// count uint32 | count × (colorLen uint32, color JPEG).
|
2026-07-13 17:47:00 +07:00
|
|
|
|
const (
|
2026-07-16 21:56:41 +07:00
|
|
|
|
tsfMagic = "TSFR"
|
|
|
|
|
|
tsfVersion = 1
|
|
|
|
|
|
maxTSFFPS = 240
|
|
|
|
|
|
maxTSFFrameCount = 10_000_000
|
2026-07-13 17:47:00 +07:00
|
|
|
|
)
|
|
|
|
|
|
|
2026-07-16 21:56:41 +07:00
|
|
|
|
type frameFile struct {
|
|
|
|
|
|
data []byte
|
|
|
|
|
|
cleanup func() error
|
|
|
|
|
|
once sync.Once
|
|
|
|
|
|
err error
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (f *frameFile) Close() error {
|
|
|
|
|
|
if f == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
f.once.Do(func() {
|
|
|
|
|
|
if f.cleanup != nil {
|
|
|
|
|
|
f.err = f.cleanup()
|
|
|
|
|
|
}
|
|
|
|
|
|
f.data = nil
|
|
|
|
|
|
})
|
|
|
|
|
|
return f.err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var frameFileOwners sync.Map // map[*FramesContainer]*frameFile
|
|
|
|
|
|
|
|
|
|
|
|
func (data *FramesContainer) Close() error {
|
|
|
|
|
|
if data == nil {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
owner, ok := frameFileOwners.LoadAndDelete(data)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
|
|
|
|
|
data.ColorFrames = nil
|
|
|
|
|
|
return owner.(*frameFile).Close()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 17:47:00 +07:00
|
|
|
|
func writeTSF(output string, data *FramesContainer) error {
|
2026-07-16 21:56:41 +07:00
|
|
|
|
if data == nil {
|
|
|
|
|
|
return fmt.Errorf("cannot write nil frames container")
|
|
|
|
|
|
}
|
|
|
|
|
|
if math.IsNaN(data.FPS) || math.IsInf(data.FPS, 0) || data.FPS <= 0 || data.FPS > maxTSFFPS {
|
|
|
|
|
|
return fmt.Errorf("cannot write .tsf: fps must be finite, positive, and at most %d", maxTSFFPS)
|
|
|
|
|
|
}
|
|
|
|
|
|
if len(data.ColorFrames) > maxTSFFrameCount || uint64(len(data.ColorFrames)) > math.MaxUint32 {
|
|
|
|
|
|
return fmt.Errorf("cannot write .tsf: frame count exceeds limit")
|
|
|
|
|
|
}
|
|
|
|
|
|
for i, frame := range data.ColorFrames {
|
|
|
|
|
|
if uint64(len(frame)) > math.MaxUint32 {
|
|
|
|
|
|
return fmt.Errorf("cannot write .tsf: frame %d length exceeds uint32", i)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 17:47:00 +07:00
|
|
|
|
f, err := os.Create(output)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
2026-07-13 23:01:48 +07:00
|
|
|
|
defer func() { _ = f.Close() }()
|
2026-07-13 17:47:00 +07:00
|
|
|
|
|
|
|
|
|
|
w := bufio.NewWriterSize(f, 1<<20)
|
|
|
|
|
|
if _, err := w.WriteString(tsfMagic); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
var hdr [14]byte
|
|
|
|
|
|
binary.LittleEndian.PutUint16(hdr[0:], tsfVersion)
|
|
|
|
|
|
binary.LittleEndian.PutUint64(hdr[2:], math.Float64bits(data.FPS))
|
2026-07-13 22:54:23 +07:00
|
|
|
|
binary.LittleEndian.PutUint32(hdr[10:], uint32(len(data.ColorFrames)))
|
2026-07-13 17:47:00 +07:00
|
|
|
|
if _, err := w.Write(hdr[:]); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var lenBuf [4]byte
|
2026-07-13 22:54:23 +07:00
|
|
|
|
for _, frame := range data.ColorFrames {
|
2026-07-13 17:47:00 +07:00
|
|
|
|
binary.LittleEndian.PutUint32(lenBuf[:], uint32(len(frame)))
|
|
|
|
|
|
if _, err := w.Write(lenBuf[:]); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
if _, err := w.Write(frame); err != nil {
|
|
|
|
|
|
return err
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return w.Flush()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func loadTSF(filename string) (*FramesContainer, error) {
|
2026-07-16 21:56:41 +07:00
|
|
|
|
file, err := readFrameFile(filename)
|
2026-07-13 17:47:00 +07:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return nil, err
|
|
|
|
|
|
}
|
2026-07-16 21:56:41 +07:00
|
|
|
|
owned := false
|
|
|
|
|
|
defer func() {
|
|
|
|
|
|
if !owned {
|
|
|
|
|
|
_ = file.Close()
|
|
|
|
|
|
}
|
|
|
|
|
|
}()
|
|
|
|
|
|
raw := file.data
|
2026-07-13 17:47:00 +07:00
|
|
|
|
invalid := func() error {
|
|
|
|
|
|
return fmt.Errorf("invalid frames file %q: corrupt .tsf container", filename)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(raw) < 18 || string(raw[:4]) != tsfMagic {
|
|
|
|
|
|
return nil, invalid()
|
|
|
|
|
|
}
|
|
|
|
|
|
version := binary.LittleEndian.Uint16(raw[4:])
|
|
|
|
|
|
if version != tsfVersion {
|
|
|
|
|
|
return nil, fmt.Errorf("unsupported .tsf version %d in %q", version, filename)
|
|
|
|
|
|
}
|
|
|
|
|
|
fps := math.Float64frombits(binary.LittleEndian.Uint64(raw[6:]))
|
|
|
|
|
|
count := binary.LittleEndian.Uint32(raw[14:])
|
2026-07-16 21:56:41 +07:00
|
|
|
|
if math.IsNaN(fps) || math.IsInf(fps, 0) || fps <= 0 || fps > maxTSFFPS {
|
2026-07-13 17:47:00 +07:00
|
|
|
|
return nil, fmt.Errorf(
|
2026-07-16 21:56:41 +07:00
|
|
|
|
"invalid frames file %q: fps must be finite, greater than 0, and at most %d",
|
|
|
|
|
|
filename, maxTSFFPS,
|
2026-07-13 17:47:00 +07:00
|
|
|
|
)
|
|
|
|
|
|
}
|
2026-07-16 21:56:41 +07:00
|
|
|
|
if count == 0 {
|
|
|
|
|
|
return nil, fmt.Errorf("invalid frames file %q: expected non-empty frames", filename)
|
|
|
|
|
|
}
|
|
|
|
|
|
if count > maxTSFFrameCount || uint64(count) > uint64((len(raw)-18)/4) {
|
|
|
|
|
|
return nil, invalid()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
colorFrames := make([][]byte, 0, int(count))
|
|
|
|
|
|
off := 18
|
|
|
|
|
|
for range count {
|
|
|
|
|
|
if len(raw)-off < 4 {
|
|
|
|
|
|
return nil, invalid()
|
|
|
|
|
|
}
|
|
|
|
|
|
n := uint64(binary.LittleEndian.Uint32(raw[off:]))
|
|
|
|
|
|
off += 4
|
|
|
|
|
|
if n > uint64(len(raw)-off) {
|
|
|
|
|
|
return nil, invalid()
|
|
|
|
|
|
}
|
|
|
|
|
|
nativeLen := int(n)
|
|
|
|
|
|
colorFrames = append(colorFrames, raw[off:off+nativeLen])
|
|
|
|
|
|
off += nativeLen
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if off != len(raw) {
|
|
|
|
|
|
return nil, invalid()
|
|
|
|
|
|
}
|
|
|
|
|
|
data := &FramesContainer{ColorFrames: colorFrames, FPS: fps}
|
|
|
|
|
|
frameFileOwners.Store(data, file)
|
|
|
|
|
|
owned = true
|
|
|
|
|
|
return data, nil
|
2026-07-13 17:47:00 +07:00
|
|
|
|
}
|