fix: Add support for X-TIMESTAMP-MAP r/w persistence (#112)

* Add support for X-TIMESTAMP-MAP r/w persistence

* Refactor TimestampMap struct per review feedback

* Fix MPEGTS / LOCAL ordering, update tests

* Clean up timestamp map vtt test
This commit is contained in:
Nick Brandt
2024-09-10 08:55:13 +02:00
committed by GitHub
parent 593487ab1c
commit f9f932dcf5
4 changed files with 65 additions and 17 deletions
+1
View File
@@ -392,6 +392,7 @@ type Metadata struct {
STLTranslatorName string
Title string
TTMLCopyright string
WebVTTTimestampMap *WebVTTTimestampMap
}
// Region represents a subtitle's region
+49 -11
View File
@@ -25,7 +25,7 @@ const (
webvttBlockNameText = "text"
webvttDefaultStyleID = "astisub-webvtt-default-style-id"
webvttTimeBoundariesSeparator = " --> "
webvttTimestampMap = "X-TIMESTAMP-MAP"
webvttTimestampMapHeader = "X-TIMESTAMP-MAP"
)
// Vars
@@ -44,11 +44,36 @@ func parseDurationWebVTT(i string) (time.Duration, error) {
return parseDuration(i, ".", 3)
}
// WebVTTTimestampMap is a structure for storing timestamps for WEBVTT's
// X-TIMESTAMP-MAP feature commonly used for syncing cue times with
// MPEG-TS streams.
type WebVTTTimestampMap struct {
Local time.Duration
MpegTS int64
}
// Offset calculates and returns the time offset described by the
// timestamp map.
func (t *WebVTTTimestampMap) Offset() time.Duration {
if t == nil {
return 0
}
return time.Duration(t.MpegTS)*time.Second/90000 - t.Local
}
// String implements Stringer interface for TimestampMap, returning
// the fully formatted header string for the instance.
func (t *WebVTTTimestampMap) String() string {
mpegts := fmt.Sprintf("MPEGTS:%d", t.MpegTS)
local := fmt.Sprintf("LOCAL:%s", formatDurationWebVTT(t.Local))
return fmt.Sprintf("%s=%s,%s", webvttTimestampMapHeader, local, mpegts)
}
// https://tools.ietf.org/html/rfc8216#section-3.5
// Eg., `X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:900000` => 10s
//
// `X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:180000` => 2s
func parseTimestampMapWebVTT(line string) (timeOffset time.Duration, err error) {
func parseWebVTTTimestampMap(line string) (timestampMap *WebVTTTimestampMap, err error) {
splits := strings.Split(line, "=")
if len(splits) <= 1 {
err = fmt.Errorf("astisub: invalid X-TIMESTAMP-MAP, no '=' found")
@@ -81,7 +106,10 @@ func parseTimestampMapWebVTT(line string) (timeOffset time.Duration, err error)
}
}
timeOffset = time.Duration(mpegts)*time.Second/90000 - local
timestampMap = &WebVTTTimestampMap{
Local: local,
MpegTS: mpegts,
}
return
}
@@ -110,7 +138,6 @@ func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
var blockName string
var comments []string
var index int
var timeOffset time.Duration
var webVTTStyles *StyleAttributes
for scanner.Scan() {
@@ -256,17 +283,22 @@ func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
// Append item
o.Items = append(o.Items, item)
case strings.HasPrefix(line, webvttTimestampMap):
case strings.HasPrefix(line, webvttTimestampMapHeader):
if len(item.Lines) > 0 {
err = errors.New("astisub: found timestamp map after processing subtitle items")
return
}
timeOffset, err = parseTimestampMapWebVTT(line)
var timestampMap *WebVTTTimestampMap
timestampMap, err = parseWebVTTTimestampMap(line)
if err != nil {
err = fmt.Errorf("astisub: parsing webvtt timestamp map failed: %w", err)
return
}
if o.Metadata == nil {
o.Metadata = new(Metadata)
}
o.Metadata.WebVTTTimestampMap = timestampMap
// Text
default:
@@ -287,10 +319,6 @@ func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
}
}
}
if timeOffset > 0 {
o.Add(timeOffset)
}
return
}
@@ -440,7 +468,17 @@ func (s Subtitles) WriteToWebVTT(o io.Writer) (err error) {
// Add header
var c []byte
c = append(c, []byte("WEBVTT\n\n")...)
c = append(c, []byte("WEBVTT")...)
// Write X-TIMESTAMP-MAP if set
if s.Metadata != nil {
webVTTTimestampMap := s.Metadata.WebVTTTimestampMap
if webVTTTimestampMap != nil {
c = append(c, []byte("\n")...)
c = append(c, []byte(webVTTTimestampMap.String())...)
}
}
c = append(c, []byte("\n\n")...)
var style []string
for _, s := range s.Styles {
+5 -4
View File
@@ -74,11 +74,11 @@ func TestTimestampMap(t *testing.T) {
expectError bool
}{
{
line: "X-TIMESTAMP-MAP=MPEGTS:180000, LOCAL:00:00:00.000",
line: "X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:180000",
expectedOffset: 2 * time.Second,
},
{
line: "X-TIMESTAMP-MAP=MPEGTS:180000, LOCAL:00:00:00.500",
line: "X-TIMESTAMP-MAP=LOCAL:00:00:00.500,MPEGTS:180000",
expectedOffset: 1500 * time.Millisecond,
},
{
@@ -107,12 +107,13 @@ func TestTimestampMap(t *testing.T) {
},
} {
t.Run(strconv.Itoa(i), func(t *testing.T) {
offset, err := parseTimestampMapWebVTT(c.line)
assert.Equal(t, c.expectedOffset, offset)
timestampMap, err := parseWebVTTTimestampMap(c.line)
assert.Equal(t, c.expectedOffset, timestampMap.Offset())
if c.expectError {
assert.Error(t, err)
} else {
assert.NoError(t, err)
assert.Equal(t, c.line, timestampMap.String())
}
})
}
+10 -2
View File
@@ -5,6 +5,7 @@ import (
"io/ioutil"
"strings"
"testing"
"time"
"github.com/asticode/go-astisub"
"github.com/stretchr/testify/assert"
@@ -117,17 +118,24 @@ func TestWebVTTWithTimestampMap(t *testing.T) {
assert.Len(t, s.Items, 2)
assert.Equal(t, s.Items[0].StartAt.Milliseconds(), int64(933))
assert.Equal(t, s.Items[0].EndAt.Milliseconds(), int64(2366))
assert.Equal(t, s.Items[1].StartAt.Milliseconds(), int64(2400))
assert.Equal(t, s.Items[1].EndAt.Milliseconds(), int64(3633))
assert.Equal(t, s.Metadata.WebVTTTimestampMap.Offset(), time.Duration(time.Second*2))
b := &bytes.Buffer{}
err = s.WriteToWebVTT(b)
assert.NoError(t, err)
assert.Equal(t, `WEBVTT
X-TIMESTAMP-MAP=LOCAL:00:00:00.000,MPEGTS:180000
1
00:00:02.933 --> 00:00:04.366
00:00:00.933 --> 00:00:02.366
♪ ♪
2
00:00:04.400 --> 00:00:05.633
00:00:02.400 --> 00:00:03.633
Evening.
`, b.String())
}