diff --git a/internal/mp4/mp4.go b/internal/mp4/mp4.go index bac519fd..52f51f5b 100644 --- a/internal/mp4/mp4.go +++ b/internal/mp4/mp4.go @@ -1,15 +1,16 @@ package mp4 import ( - "github.com/AlexxIT/go2rtc/internal/api/ws" "net/http" "strconv" "strings" "time" "github.com/AlexxIT/go2rtc/internal/api" + "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/tcp" "github.com/rs/zerolog" @@ -153,6 +154,16 @@ func handlerMP4(w http.ResponseWriter, r *http.Request) { header.Set("Content-Disposition", `attachment; filename="`+filename+`"`) } + if rotate := query.Get("rotate"); rotate != "" { + mp4.PatchVideoRotate(data, core.Atoi(rotate)) + } + + if scale := query.Get("scale"); scale != "" { + if sx, sy, ok := strings.Cut(scale, ":"); ok { + mp4.PatchVideoScale(data, core.Atoi(sx), core.Atoi(sy)) + } + } + if _, err = w.Write(data); err != nil { log.Error().Err(err).Caller().Send() http.Error(w, err.Error(), http.StatusInternalServerError) diff --git a/pkg/iso/codecs.go b/pkg/iso/codecs.go index 9f11428b..4b74847a 100644 --- a/pkg/iso/codecs.go +++ b/pkg/iso/codecs.go @@ -41,6 +41,11 @@ func (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) { m.Write(conf) m.EndAtom() // AVCC + m.StartAtom("pasp") // Pixel Aspect Ratio + m.WriteUint32(1) // hSpacing + m.WriteUint32(1) // vSpacing + m.EndAtom() + m.EndAtom() // AVC1 } diff --git a/pkg/mp4/helpers.go b/pkg/mp4/helpers.go index fc2a9cf9..ff737d29 100644 --- a/pkg/mp4/helpers.go +++ b/pkg/mp4/helpers.go @@ -1,8 +1,11 @@ package mp4 import ( - "github.com/AlexxIT/go2rtc/pkg/core" + "bytes" + "encoding/binary" "strings" + + "github.com/AlexxIT/go2rtc/pkg/core" ) // ParseQuery - like usual parse, but with mp4 param handler @@ -99,6 +102,67 @@ func ParseCodecs(codecs string, parseAudio bool) (medias []*core.Media) { return } +// PatchVideoRotate - update video track transformation matrix. +// Rotation supported by many players and browsers (except Safari). +// Scale has low support and better not to use it. +// Supported only 0, 90, 180, 270 degrees. +func PatchVideoRotate(init []byte, degrees int) bool { + // search video atom + i := bytes.Index(init, []byte("vide")) + if i < 0 { + return false + } + + // seek to video matrix position + i -= 4 + 3 + 1 + 8 + 32 + 8 + 4 + 4 + 4*9 + + // Rotation matrix: + // [ cos sin 0] + // [ -sin cos 0] + // [ 0 0 16384] + var cos, sin uint16 + + switch degrees { + case 0: + cos = 1 + sin = 0 + case 90: + cos = 0 + sin = 1 + case 180: + cos = 0xFFFF // -1 + sin = 0 + case 270: + cos = 0 + sin = 0xFFFF // -1 + default: + return false + } + + binary.BigEndian.PutUint16(init[i:], cos) + binary.BigEndian.PutUint16(init[i+4:], sin) + binary.BigEndian.PutUint16(init[i+12:], -sin) + binary.BigEndian.PutUint16(init[i+16:], cos) + + return true +} + +// PatchVideoScale - update "Pixel Aspect Ratio" atom. +// Supported by many players and browsers (except Firefox). +// Supported only positive integers. +func PatchVideoScale(init []byte, scaleX, scaleY int) bool { + // search video atom + i := bytes.Index(init, []byte("pasp")) + if i < 0 { + return false + } + + binary.BigEndian.PutUint32(init[i+4:], uint32(scaleX)) + binary.BigEndian.PutUint32(init[i+8:], uint32(scaleY)) + + return true +} + const ( stateNone byte = iota stateInit