diff --git a/lib/cea/cea708_service.js b/lib/cea/cea708_service.js index 138db6358e..d5db9e9084 100644 --- a/lib/cea/cea708_service.js +++ b/lib/cea/cea708_service.js @@ -62,7 +62,7 @@ shaka.cea.Cea708Service = class { // Control codes are in 1 of 4 logical groups: // CL (C0, C2), CR (C1, C3), GL (G0, G2), GR (G1, G2). if (controlCode >= 0x00 && controlCode <= 0x1f) { - return this.handleC0_(controlCode, pts); + return this.handleC0_(dtvccPacket, controlCode, pts); } else if (controlCode >= 0x80 && controlCode <= 0x9f) { return this.handleC1_(dtvccPacket, controlCode, pts); } else if (controlCode >= 0x1000 && controlCode <= 0x101f) { @@ -155,17 +155,42 @@ shaka.cea.Cea708Service = class { /** * Handles C0 group data. + * @param {!shaka.cea.DtvccPacket} dtvccPacket * @param {number} controlCode * @param {number} pts * @return {?shaka.extern.ICaptionDecoder.ClosedCaption} * @private */ - handleC0_(controlCode, pts) { + handleC0_(dtvccPacket, controlCode, pts) { // All these commands pertain to the current window, so ensure it exists. if (!this.currentWindow_) { return null; } + if (controlCode == 0x18) { + const firstByte = dtvccPacket.readByte().value; + const secondByte = dtvccPacket.readByte().value; + + const isTextBlock = (b) => { + return (b >= 0x20 && b <= 0x7f) || (b >= 0xa0 && b <= 0xff); + }; + + if (isTextBlock(firstByte) && isTextBlock(secondByte)) { + const toHexString = (byteArray) => { + return byteArray.map((byte) => { + return ('0' + (byte & 0xFF).toString(16)).slice(-2); + }).join(''); + }; + const unicode = toHexString([firstByte, secondByte]); + // Takes a unicode hex string and creates a single character. + const char = String.fromCharCode(parseInt(unicode, 16)); + this.currentWindow_.setCharacter(char); + return null; + } else { + dtvccPacket.rewind(2); + } + } + const window = this.currentWindow_; let parsedClosedCaption = null; diff --git a/lib/cea/dtvcc_packet_builder.js b/lib/cea/dtvcc_packet_builder.js index 5078557b26..ec24d9f6ab 100644 --- a/lib/cea/dtvcc_packet_builder.js +++ b/lib/cea/dtvcc_packet_builder.js @@ -161,6 +161,21 @@ shaka.cea.DtvccPacket = class { } this.pos_ += numBlocks; } + + /** + * Rewinds the provided number of blocks in the buffer. + * @param {number} numBlocks + * @throws {!shaka.util.Error} + */ + rewind(numBlocks) { + if (this.pos_ - numBlocks < 0) { + throw new shaka.util.Error( + shaka.util.Error.Severity.CRITICAL, + shaka.util.Error.Category.TEXT, + shaka.util.Error.Code.BUFFER_READ_OUT_OF_BOUNDS); + } + this.pos_ -= numBlocks; + } }; /** diff --git a/test/cea/cea708_service_unit.js b/test/cea/cea708_service_unit.js index d44cedecb0..7d1938cfb1 100644 --- a/test/cea/cea708_service_unit.js +++ b/test/cea/cea708_service_unit.js @@ -123,6 +123,35 @@ describe('Cea708Service', () => { expect(captions).toEqual(expectedCaptions); }); + + it('decodes multibyte unstyled caption text', () => { + const controlCodes = [ + ...defineWindow, + // Series of C0 control codes that add multi-byte text. + 0x18, 0xb9, 0xd9, 0x18, 0xb7, 0xce, // 맙, 럎 + ]; + + const packet1 = createCea708PacketFromBytes(controlCodes, startTime); + const packet2 = createCea708PacketFromBytes(hideWindow, endTime); + + const text = '맙럎'; + const topLevelCue = CeaUtils.createWindowedCue(startTime, endTime, '', + serviceNumber, windowId, rowCount, colCount, anchorId); + topLevelCue.nestedCues = [ + CeaUtils.createDefaultCue(startTime, endTime, /* payload= */ text), + ]; + + const expectedCaptions = [ + { + stream, + cue: topLevelCue, + }, + ]; + + const captions = getCaptionsFromPackets(service, packet1, packet2); + expect(captions).toEqual(expectedCaptions); + }); + it('setPenLocation sets the pen location correctly', () => { const controlCodes = [ ...defineWindow,