Skip to content

Commit

Permalink
Added teletext
Browse files Browse the repository at this point in the history
  • Loading branch information
Quentin Renard committed Feb 5, 2018
1 parent 0074155 commit df3a039
Show file tree
Hide file tree
Showing 3 changed files with 358 additions and 12 deletions.
19 changes: 9 additions & 10 deletions subtitles.go
Expand Up @@ -32,31 +32,30 @@ var Now = func() time.Time {

// Options represents open or write options
type Options struct {
Page int
PID int
Src string
Filename string
Teletext TeletextOptions
}

// Open opens a subtitle file based on options
// Open opens a subtitle reader based on options
func Open(o Options) (s *Subtitles, err error) {
// Open the file
var f *os.File
if f, err = os.Open(o.Src); err != nil {
err = errors.Wrapf(err, "astisub: opening %s failed", o.Src)
if f, err = os.Open(o.Filename); err != nil {
err = errors.Wrapf(err, "astisub: opening %s failed", o.Filename)
return
}
defer f.Close()

// Parse the content
switch filepath.Ext(o.Src) {
switch filepath.Ext(o.Filename) {
case ".srt":
s, err = ReadFromSRT(f)
case ".ssa", ".ass":
s, err = ReadFromSSA(f)
case ".stl":
s, err = ReadFromSTL(f)
case ".ts":
s, err = ReadFromTeletext(f, o.PID, o.Page)
s, err = ReadFromTeletext(f, o.Teletext)
case ".ttml":
s, err = ReadFromTTML(f)
case ".vtt":
Expand All @@ -68,8 +67,8 @@ func Open(o Options) (s *Subtitles, err error) {
}

// OpenFile opens a file regardless of other options
func OpenFile(src string) (*Subtitles, error) {
return Open(Options{Src: src})
func OpenFile(filename string) (*Subtitles, error) {
return Open(Options{Filename: filename})
}

// Subtitles represents an ordered list of items with formatting
Expand Down
299 changes: 297 additions & 2 deletions teletext.go
@@ -1,8 +1,303 @@
package astisub

import "io"
import (
"context"
"fmt"
"io"

"github.com/asticode/go-astits"
"github.com/pkg/errors"
)

// Errors
var (
ErrNoValidTeletextPID = errors.New("astisub: no valid teletext PID")
)

// Teletext PES data types
const (
teletextPESDataTypeEBU = "EBU"
teletextPESDataTypeUnknown = "unknown"
)

// Teletext PES data unit types
const (
teletextPESDataUnitTypeEBUNonSubtitleData = 0x2
teletextPESDataUnitTypeEBUSubtitleData = 0x3
teletextPESDataUnitTypeStuffing = 0xff
)

// TeletextOptions represents teletext options
type TeletextOptions struct {
Page int
PID int
}

// ReadFromTeletext parses a teletext content
func ReadFromTeletext(r io.ReadSeeker, pid, page int) (o *Subtitles, err error) {
// http://www.etsi.org/deliver/etsi_en/300400_300499/300472/01.03.01_60/en_300472v010301p.pdf
// http://www.etsi.org/deliver/etsi_i_ets/300700_300799/300706/01_60/ets_300706e01p.pdf
func ReadFromTeletext(r io.Reader, o TeletextOptions) (s *Subtitles, err error) {
// Init demuxer
var dmx = astits.New(context.Background(), r)

// Get the teletext PID
var pid uint16
if pid, err = teletextPID(dmx, o); err != nil {
if err != ErrNoValidTeletextPID {
err = errors.Wrap(err, "astisub: getting teletext PID failed")
}
return
}

// Loop in data
var d *astits.Data
for {
// Fetch next data
if d, err = dmx.NextData(); err != nil {
if err == astits.ErrNoMorePackets {
err = nil
break
}
err = errors.Wrap(err, "astisub: fetching next data failed")
return
}

// This data is not of interest to us
if d.PID != pid || d.PES == nil || d.PES.Header.StreamID != astits.StreamIDPrivateStream1 {
continue
}

// Parse PES data
var td *teletextPESData
if td, err = parseTeletextPESData(d.PES.Data); err != nil {
err = errors.Wrap(err, "astisub: parsing teletext PES data failed")
return
}
_ = td
}
return
}

// teletextPID returns the teletext PID.
// If the PID teletext option is not indicated, it will walk through the ts data until it reaches a PMT packet to
// detect the first valid teletext PID
func teletextPID(dmx *astits.Demuxer, o TeletextOptions) (pid uint16, err error) {
// PID is in the options
if o.PID > 0 {
pid = uint16(o.PID)
return
}

// Loop in data
var d *astits.Data
for {
// Fetch next data
if d, err = dmx.NextData(); err != nil {
if err == astits.ErrNoMorePackets {
err = ErrNoValidTeletextPID
return
}
err = errors.Wrap(err, "astisub: fetching next data failed")
return
}

// PMT data
if d.PMT != nil {
// Retrieve valid teletext PIDs
var pids []uint16
for _, s := range d.PMT.ElementaryStreams {
for _, dsc := range s.ElementaryStreamDescriptors {
if dsc.Tag == astits.DescriptorTagTeletext || dsc.Tag == astits.DescriptorTagVBITeletext {
pids = append(pids, s.ElementaryPID)
}
}
}

// No valid teletext PIDs
if len(pids) == 0 {
err = ErrNoValidTeletextPID
return
}

// Set pid
pid = pids[0]

// Rewind
if _, err = dmx.Rewind(); err != nil {
err = errors.Wrap(err, "astisub: rewinding failed")
return
}
return
}
}
return
}

// teletextPESData represents a teletext PES data
type teletextPESData struct {
dataIdentifier uint8
units []*teletextPESDataUnit
}

// teletextPESDataUnit represents a teletext PES data unit
type teletextPESDataUnit struct {
data []byte
designationCode uint8
fieldParity bool
framingCode uint8
id uint8
length uint8
lineOffset uint8
magazineNumber uint8
packetNumber uint8
}

// LineOffsetNumber returns the teletext data unit line offset number
func (u teletextPESDataUnit) LineOffsetNumber() int {
if u.lineOffset < 0x7 || u.lineOffset > 0x16 {
return 0
}
var offset int
if !u.fieldParity {
offset = 313
}
return int(u.lineOffset) + offset
}

// parseTeletextPESData parses a Teletext PES data
func parseTeletextPESData(i []byte) (d *teletextPESData, err error) {
// Init
d = &teletextPESData{}
var offset int

// Data identifier
d.dataIdentifier = uint8(i[offset])
offset += 1

// Loop until end of data
for offset < len(i) {
// Parse data unit
parseTeletextPESDataUnit(i, &offset, d)
}
return
}

// parseTeletextPESData parses a Teletext PES data unit
func parseTeletextPESDataUnit(i []byte, offset *int, d *teletextPESData) {
// Init
var u = &teletextPESDataUnit{}

// ID
u.id = uint8(i[*offset])
*offset += 1

// Length
u.length = uint8(i[*offset])
*offset += 1

// Make sure we seek at the end of the data unit once everything is done
var offsetEnd = *offset + int(u.length)
defer func(offset *int) {
*offset = offsetEnd
}(offset)

// Unprocessed data unit ids
// TODO Should we process other ids?
if u.id != teletextPESDataUnitTypeEBUSubtitleData {
return
}

// Field parity
u.fieldParity = i[*offset]&0x20 > 0

// Line offset
u.lineOffset = uint8(i[*offset] & 0x1f)
*offset += 1

// Framing code
u.framingCode = uint8(i[*offset])
*offset += 1

// Framing code must be 11100100
if u.framingCode != 0xe4 {
return
}

// Magazine number and packet number
var h, h1, h2 uint8
var errHamming error
if h1, errHamming = hamming84Decode(i[*offset]); errHamming != nil {
return
}
if h2, errHamming = hamming84Decode(i[*offset+1]); errHamming != nil {
return
}
h = h2<<4 | h1
u.magazineNumber = h & 0x7
if u.magazineNumber == 0 {
u.magazineNumber = 8
}
u.packetNumber = h >> 3
*offset += 2

// Designation code
if u.packetNumber > 25 {
if u.designationCode, errHamming = hamming84Decode(i[*offset]); errHamming != nil {
return
}
*offset += 1
}

// Data
u.data = i[*offset:offsetEnd]

// Append data unit
d.units = append(d.units, u)
return
}

// teletextPESDataType returns the teletext PES data type based on the data identifier
func teletextPESDataType(dataIdentifier uint8) string {
switch {
case dataIdentifier >= 0x10 && dataIdentifier <= 0x1f:
return teletextPESDataTypeEBU
}
return teletextPESDataTypeUnknown
}

// hamming84Decode decodes a Hamming 8/4
func hamming84Decode(i byte) (o uint8, err error) {
p1, d1, p2, d2, p3, d3, p4, d4 := i>>7&0x1, i>>6&0x1, i>>5&0x1, i>>4&0x1, i>>3&0x1, i>>2&0x1, i>>1&0x1, i&0x1
testA := p1^d1^d3^d4 > 0
testB := d1^p2^d2^d4 > 0
testC := d1^d2^p3^d3 > 0
testD := p1^d1^p2^d2^p3^d3^p4^d4 > 0
if testA && testB && testC {
// p4 may be incorrect
} else if testD && (!testA || !testB || !testC) {
err = fmt.Errorf("hamming 8/4 decode of %.8b failed", i)
return
} else {
if !testA && testB && testC {
// p1 is incorrect
} else if testA && !testB && testC {
// p2 is incorrect
} else if testA && testB && !testC {
// p3 is incorrect
} else if !testA && !testB && testC {
// d4 is incorrect
d4 ^= 1
} else if testA && !testB && !testC {
// d2 is incorrect
d2 ^= 1
} else if !testA && testB && !testC {
// d3 is incorrect
d3 ^= 1
} else {
// d1 is incorrect
d1 ^= 1
}
}
o = uint8(d4<<3 | d3<<2 | d2<<1 | d1)
return
}

0 comments on commit df3a039

Please sign in to comment.