Skip to content

Commit

Permalink
fix: Add support for X-TIMESTAMP-MAP r/w persistence (#112)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
nakkamarra authored Sep 10, 2024
1 parent 593487a commit f9f932d
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 17 deletions.
1 change: 1 addition & 0 deletions subtitles.go
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,7 @@ type Metadata struct {
STLTranslatorName string
Title string
TTMLCopyright string
WebVTTTimestampMap *WebVTTTimestampMap
}

// Region represents a subtitle's region
Expand Down
60 changes: 49 additions & 11 deletions webvtt.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const (
webvttBlockNameText = "text"
webvttDefaultStyleID = "astisub-webvtt-default-style-id"
webvttTimeBoundariesSeparator = " --> "
webvttTimestampMap = "X-TIMESTAMP-MAP"
webvttTimestampMapHeader = "X-TIMESTAMP-MAP"
)

// Vars
Expand All @@ -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")
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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:
Expand All @@ -287,10 +319,6 @@ func ReadFromWebVTT(i io.Reader) (o *Subtitles, err error) {
}
}
}

if timeOffset > 0 {
o.Add(timeOffset)
}
return
}

Expand Down Expand Up @@ -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 {
Expand Down
9 changes: 5 additions & 4 deletions webvtt_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand Down Expand Up @@ -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())
}
})
}
Expand Down
12 changes: 10 additions & 2 deletions webvtt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/ioutil"
"strings"
"testing"
"time"

"github.com/asticode/go-astisub"
"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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())
}
Expand Down

0 comments on commit f9f932d

Please sign in to comment.