Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Quentin Renard
committed
Feb 5, 2018
1 parent
0074155
commit df3a039
Showing
3 changed files
with
358 additions
and
12 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.