| /**********************************************************************************************/ | |
| /* The MIT License */ | |
| /* */ | |
| /* Copyright 2016-2017 Twitch Interactive, Inc. or its affiliates. All Rights Reserved. */ | |
| /* */ | |
| /* Permission is hereby granted, free of charge, to any person obtaining a copy */ | |
| /* of this software and associated documentation files (the "Software"), to deal */ | |
| /* in the Software without restriction, including without limitation the rights */ | |
| /* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell */ | |
| /* copies of the Software, and to permit persons to whom the Software is */ | |
| /* furnished to do so, subject to the following conditions: */ | |
| /* */ | |
| /* The above copyright notice and this permission notice shall be included in */ | |
| /* all copies or substantial portions of the Software. */ | |
| /* */ | |
| /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR */ | |
| /* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, */ | |
| /* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE */ | |
| /* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER */ | |
| /* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, */ | |
| /* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN */ | |
| /* THE SOFTWARE. */ | |
| /**********************************************************************************************/ | |
| #include "caption.h" | |
| #include "eia608.h" | |
| #include "utf8.h" | |
| #include "xds.h" | |
| #include <stdio.h> | |
| #include <string.h> | |
| //////////////////////////////////////////////////////////////////////////////// | |
| void | |
| caption_frame_buffer_clear (caption_frame_buffer_t * buff) | |
| { | |
| memset (buff, 0, sizeof (caption_frame_buffer_t)); | |
| } | |
| void | |
| caption_frame_state_clear (caption_frame_t * frame) | |
| { | |
| frame->write = 0; | |
| frame->timestamp = -1; | |
| frame->state = (caption_frame_state_t) { | |
| 0, 0, 0, SCREEN_ROWS - 1, 0, 0}; // clear global state | |
| } | |
| void | |
| caption_frame_init (caption_frame_t * frame) | |
| { | |
| xds_init (&frame->xds); | |
| caption_frame_state_clear (frame); | |
| caption_frame_buffer_clear (&frame->back); | |
| caption_frame_buffer_clear (&frame->front); | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| // Helpers | |
| static caption_frame_cell_t * | |
| frame_buffer_cell (caption_frame_buffer_t * buff, int row, int col) | |
| { | |
| if (!buff || 0 > row || SCREEN_ROWS <= row || 0 > col || SCREEN_COLS <= col) { | |
| return 0; | |
| } | |
| return &buff->cell[row][col]; | |
| } | |
| int caption_frame_clear_char (caption_frame_t * frame, int row, int col) | |
| { | |
| if (!frame->write) { | |
| return 0; | |
| } | |
| caption_frame_cell_t *cell = frame_buffer_cell (frame->write, row, col); | |
| if (cell) { | |
| memset (cell, 0, sizeof (caption_frame_cell_t)); | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| uint16_t _eia608_from_utf8 (const char *s); // function is in eia608.c.re2c | |
| int | |
| caption_frame_write_char (caption_frame_t * frame, int row, int col, | |
| eia608_style_t style, int underline, const char *c) | |
| { | |
| if (!frame->write || !_eia608_from_utf8 (c)) { | |
| return 0; | |
| } | |
| caption_frame_cell_t *cell = frame_buffer_cell (frame->write, row, col); | |
| if (cell && utf8_char_copy (&cell->data[0], c)) { | |
| cell->uln = underline; | |
| cell->sty = style; | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| const utf8_char_t * | |
| caption_frame_read_char (caption_frame_t * frame, int row, int col, | |
| eia608_style_t * style, int *underline) | |
| { | |
| // always read from front | |
| caption_frame_cell_t *cell = frame_buffer_cell (&frame->front, row, col); | |
| if (!cell) { | |
| if (style) { | |
| (*style) = eia608_style_white; | |
| } | |
| if (underline) { | |
| (*underline) = 0; | |
| } | |
| return EIA608_CHAR_NULL; | |
| } | |
| if (style) { | |
| (*style) = cell->sty; | |
| } | |
| if (underline) { | |
| (*underline) = cell->uln; | |
| } | |
| return &cell->data[0]; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| // Parsing | |
| libcaption_stauts_t | |
| caption_frame_carriage_return (caption_frame_t * frame) | |
| { | |
| if (0 > frame->state.row || SCREEN_ROWS <= frame->state.row) { | |
| return LIBCAPTION_ERROR; | |
| } | |
| int r = frame->state.row - (frame->state.rup - 1); | |
| if (0 >= r || !caption_frame_rollup (frame)) { | |
| return LIBCAPTION_OK; | |
| } | |
| for (; r < SCREEN_ROWS; ++r) { | |
| uint8_t *dst = (uint8_t *) frame_buffer_cell (frame->write, r - 1, 0); | |
| uint8_t *src = (uint8_t *) frame_buffer_cell (frame->write, r - 0, 0); | |
| memcpy (dst, src, sizeof (caption_frame_cell_t) * SCREEN_COLS); | |
| } | |
| frame->state.col = 0; | |
| caption_frame_cell_t *cell = | |
| frame_buffer_cell (frame->write, SCREEN_ROWS - 1, 0); | |
| memset (cell, 0, sizeof (caption_frame_cell_t) * SCREEN_COLS); | |
| return LIBCAPTION_OK; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| libcaption_stauts_t | |
| eia608_write_char (caption_frame_t * frame, char *c) | |
| { | |
| if (0 == c || 0 == c[0] || SCREEN_ROWS <= frame->state.row | |
| || 0 > frame->state.row || SCREEN_COLS <= frame->state.col | |
| || 0 > frame->state.col) { | |
| // NO-OP | |
| } else if (caption_frame_write_char (frame, frame->state.row, | |
| frame->state.col, frame->state.sty, frame->state.uln, c)) { | |
| frame->state.col += 1; | |
| } | |
| return LIBCAPTION_OK; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_end (caption_frame_t * frame) | |
| { | |
| memcpy (&frame->front, &frame->back, sizeof (caption_frame_buffer_t)); | |
| caption_frame_buffer_clear (&frame->back); // This is required | |
| return LIBCAPTION_READY; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_decode_preamble (caption_frame_t * frame, uint16_t cc_data) | |
| { | |
| eia608_style_t sty; | |
| int row, col, chn, uln; | |
| if (eia608_parse_preamble (cc_data, &row, &col, &sty, &chn, &uln)) { | |
| frame->state.row = row; | |
| frame->state.col = col; | |
| frame->state.sty = sty; | |
| frame->state.uln = uln; | |
| } | |
| return LIBCAPTION_OK; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_decode_midrowchange (caption_frame_t * frame, uint16_t cc_data) | |
| { | |
| eia608_style_t sty; | |
| int chn, unl; | |
| if (eia608_parse_midrowchange (cc_data, &chn, &sty, &unl)) { | |
| frame->state.sty = sty; | |
| frame->state.uln = unl; | |
| } | |
| return LIBCAPTION_OK; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_backspace (caption_frame_t * frame) | |
| { | |
| // do not reverse wrap (tw 28:20) | |
| frame->state.col = (0 < frame->state.col) ? (frame->state.col - 1) : 0; | |
| caption_frame_clear_char (frame, frame->state.row, frame->state.col); | |
| return LIBCAPTION_READY; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_delete_to_end_of_row (caption_frame_t * frame) | |
| { | |
| int c; | |
| if (frame->write) { | |
| for (c = frame->state.col; c < SCREEN_COLS; ++c) { | |
| caption_frame_clear_char (frame, frame->state.row, c); | |
| } | |
| } | |
| // TODO test this and replace loop | |
| // uint8_t* dst = (uint8_t*)frame_buffer_cell(frame->write, frame->state.row, frame->state.col); | |
| // memset(dst,0,sizeof(caption_frame_cell_t) * (SCREEN_COLS - frame->state.col - 1)) | |
| return LIBCAPTION_READY; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_decode_control (caption_frame_t * frame, uint16_t cc_data) | |
| { | |
| int cc; | |
| eia608_control_t cmd = eia608_parse_control (cc_data, &cc); | |
| switch (cmd) { | |
| // PAINT ON | |
| case eia608_control_resume_direct_captioning: | |
| frame->state.rup = 0; | |
| frame->write = &frame->front; | |
| return LIBCAPTION_OK; | |
| case eia608_control_erase_display_memory: | |
| caption_frame_buffer_clear (&frame->front); | |
| return LIBCAPTION_CLEAR; | |
| // ROLL-UP | |
| case eia608_control_roll_up_2: | |
| frame->state.rup = 1; | |
| frame->write = &frame->front; | |
| return LIBCAPTION_OK; | |
| case eia608_control_roll_up_3: | |
| frame->state.rup = 2; | |
| frame->write = &frame->front; | |
| return LIBCAPTION_OK; | |
| case eia608_control_roll_up_4: | |
| frame->state.rup = 3; | |
| frame->write = &frame->front; | |
| return LIBCAPTION_OK; | |
| case eia608_control_carriage_return: | |
| return caption_frame_carriage_return (frame); | |
| // Corrections (Is this only valid as part of paint on?) | |
| case eia608_control_backspace: | |
| return caption_frame_backspace (frame); | |
| case eia608_control_delete_to_end_of_row: | |
| return caption_frame_delete_to_end_of_row (frame); | |
| // POP ON | |
| case eia608_control_resume_caption_loading: | |
| frame->state.rup = 0; | |
| frame->write = &frame->back; | |
| return LIBCAPTION_OK; | |
| case eia608_control_erase_non_displayed_memory: | |
| caption_frame_buffer_clear (&frame->back); | |
| return LIBCAPTION_OK; | |
| case eia608_control_end_of_caption: | |
| return caption_frame_end (frame); | |
| // cursor positioning | |
| case eia608_tab_offset_0: | |
| case eia608_tab_offset_1: | |
| case eia608_tab_offset_2: | |
| case eia608_tab_offset_3: | |
| frame->state.col += (cmd - eia608_tab_offset_0); | |
| return LIBCAPTION_OK; | |
| // Unhandled | |
| default: | |
| case eia608_control_alarm_off: | |
| case eia608_control_alarm_on: | |
| case eia608_control_text_restart: | |
| case eia608_control_text_resume_text_display: | |
| return LIBCAPTION_OK; | |
| } | |
| } | |
| libcaption_stauts_t | |
| caption_frame_decode_text (caption_frame_t * frame, uint16_t cc_data) | |
| { | |
| int chan; | |
| char char1[5], char2[5]; | |
| size_t chars = eia608_to_utf8 (cc_data, &chan, &char1[0], &char2[0]); | |
| if (eia608_is_westeu (cc_data)) { | |
| // Extended charcters replace the previous charcter for back compatibility | |
| caption_frame_backspace (frame); | |
| } | |
| if (0 < chars) { | |
| eia608_write_char (frame, char1); | |
| } | |
| if (1 < chars) { | |
| eia608_write_char (frame, char2); | |
| } | |
| return LIBCAPTION_OK; | |
| } | |
| libcaption_stauts_t | |
| caption_frame_decode (caption_frame_t * frame, uint16_t cc_data, | |
| double timestamp) | |
| { | |
| if (!eia608_parity_varify (cc_data)) { | |
| frame->status = LIBCAPTION_ERROR; | |
| return frame->status; | |
| } | |
| if (eia608_is_padding (cc_data)) { | |
| frame->status = LIBCAPTION_OK; | |
| return frame->status; | |
| } | |
| // skip duplicate controll commands. We also skip duplicate specialna to match the behaviour of iOS/vlc | |
| if ((eia608_is_specialna(cc_data) || eia608_is_control(cc_data)) && cc_data == frame->state.cc_data) { | |
| if (timestamp < 0 && caption_frame_popon (frame)) | |
| frame->timestamp += (1 / 29.97); | |
| return LIBCAPTION_OK; | |
| } | |
| if (timestamp >= 0) { | |
| frame->timestamp = timestamp; | |
| frame->status = LIBCAPTION_OK; | |
| } else if (caption_frame_popon (frame)) { | |
| frame->timestamp += (1 / 29.97); | |
| } | |
| frame->state.cc_data = cc_data; | |
| if (frame->xds.state) { | |
| frame->status = xds_decode (&frame->xds, cc_data); | |
| } else if (eia608_is_xds (cc_data)) { | |
| frame->status = xds_decode (&frame->xds, cc_data); | |
| } else if (eia608_is_control (cc_data)) { | |
| frame->status = caption_frame_decode_control (frame, cc_data); | |
| } else if (eia608_is_basicna (cc_data) || eia608_is_specialna (cc_data) | |
| || eia608_is_westeu (cc_data)) { | |
| // Don't decode text if we dont know what mode we are in. | |
| if (!frame->write) { | |
| frame->status = LIBCAPTION_OK; | |
| return frame->status; | |
| } | |
| frame->status = caption_frame_decode_text (frame, cc_data); | |
| // If we are in paint on mode, display immiditally | |
| if (LIBCAPTION_OK == frame->status && caption_frame_painton (frame)) { | |
| frame->status = LIBCAPTION_READY; | |
| } | |
| } else if (eia608_is_preamble (cc_data)) { | |
| frame->status = caption_frame_decode_preamble (frame, cc_data); | |
| } else if (eia608_is_midrowchange (cc_data)) { | |
| frame->status = caption_frame_decode_midrowchange (frame, cc_data); | |
| } | |
| return frame->status; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| int | |
| caption_frame_from_text (caption_frame_t * frame, const utf8_char_t * data) | |
| { | |
| ssize_t size = (ssize_t) strlen (data); | |
| caption_frame_init (frame); | |
| frame->write = &frame->back; | |
| for (size_t r = 0; (*data) && size && r < SCREEN_ROWS;) { | |
| // skip whitespace at start of line | |
| while (size && utf8_char_whitespace (data)) { | |
| size_t s = utf8_char_length (data); | |
| data += s, size -= s; | |
| } | |
| // get charcter count for wrap (or orest of line) | |
| utf8_size_t char_count = utf8_wrap_length (data, SCREEN_COLS); | |
| // write to caption frame | |
| for (size_t c = 0; c < char_count; ++c) { | |
| size_t char_length = utf8_char_length (data); | |
| caption_frame_write_char (frame, r, c, eia608_style_white, 0, data); | |
| data += char_length, size -= char_length; | |
| } | |
| r += char_count ? 1 : 0; // Update row num only if not blank | |
| } | |
| caption_frame_end (frame); | |
| return 0; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| size_t | |
| caption_frame_to_text (caption_frame_t * frame, utf8_char_t * data, int full) | |
| { | |
| int r, c, uln, crlf = 0, count = 0; | |
| size_t s, size = 0; | |
| eia608_style_t sty; | |
| (*data) = '\0'; | |
| for (r = 0; r < SCREEN_ROWS; ++r) { | |
| crlf += count, count = 0; | |
| for (c = 0; c < SCREEN_COLS; ++c) { | |
| const utf8_char_t *chr = | |
| caption_frame_read_char (frame, r, c, &sty, &uln); | |
| // dont start a new line until we encounter at least one printable character | |
| if (full || | |
| (0 < utf8_char_length (chr) && (0 < count | |
| || !utf8_char_whitespace (chr)))) { | |
| if (0 < crlf) { | |
| memcpy (data, "\r\n\0", 3); | |
| data += 2, size += 2, crlf = 0; | |
| } | |
| s = utf8_char_copy (data, chr); | |
| data += s, size += s, ++count; | |
| } | |
| } | |
| } | |
| return size; | |
| } | |
| //////////////////////////////////////////////////////////////////////////////// | |
| size_t | |
| caption_frame_dump_buffer (caption_frame_t * frame, utf8_char_t * buf) | |
| { | |
| int r, c; | |
| size_t bytes, total = 0; | |
| bytes = | |
| sprintf (buf, | |
| " timestamp: %f\n row: %02d col: %02d roll-up: %d\n", | |
| frame->timestamp, frame->state.row, frame->state.col, | |
| caption_frame_rollup (frame)); | |
| total += bytes, buf += bytes; | |
| bytes = | |
| sprintf (buf, | |
| " 00000000001111111111222222222233\t 00000000001111111111222222222233\n" | |
| " 01234567890123456789012345678901\t 01234567890123456789012345678901\n" | |
| " %s--------------------------------%s\t %s--------------------------------%s\n", | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_DOWN_AND_RIGHT, | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_DOWN_AND_LEFT, | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_DOWN_AND_RIGHT, | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_DOWN_AND_LEFT); | |
| total += bytes; | |
| buf += bytes; | |
| for (r = 0; r < SCREEN_ROWS; ++r) { | |
| bytes = sprintf (buf, "%02d%s", r, EIA608_CHAR_VERTICAL_LINE); | |
| total += bytes, buf += bytes; | |
| // front buffer | |
| for (c = 0; c < SCREEN_COLS; ++c) { | |
| caption_frame_cell_t *cell = frame_buffer_cell (&frame->front, r, c); | |
| bytes = utf8_char_copy (buf, (!cell | |
| || 0 == cell->data[0]) ? EIA608_CHAR_SPACE : &cell->data[0]); | |
| total += bytes, buf += bytes; | |
| } | |
| bytes = | |
| sprintf (buf, "%s\t%02d%s", EIA608_CHAR_VERTICAL_LINE, r, | |
| EIA608_CHAR_VERTICAL_LINE); | |
| total += bytes, buf += bytes; | |
| // back buffer | |
| for (c = 0; c < SCREEN_COLS; ++c) { | |
| caption_frame_cell_t *cell = frame_buffer_cell (&frame->back, r, c); | |
| bytes = utf8_char_copy (buf, (!cell | |
| || 0 == cell->data[0]) ? EIA608_CHAR_SPACE : &cell->data[0]); | |
| total += bytes, buf += bytes; | |
| } | |
| bytes = sprintf (buf, "%s\n", EIA608_CHAR_VERTICAL_LINE); | |
| total += bytes, buf += bytes; | |
| } | |
| bytes = | |
| sprintf (buf, | |
| " %s--------------------------------%s\t %s--------------------------------%s\n", | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_UP_AND_RIGHT, | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_UP_AND_LEFT, | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_UP_AND_RIGHT, | |
| EIA608_CHAR_BOX_DRAWINGS_LIGHT_UP_AND_LEFT); | |
| total += bytes, buf += bytes; | |
| return total; | |
| } | |
| void | |
| caption_frame_dump (caption_frame_t * frame) | |
| { | |
| utf8_char_t buff[CAPTION_FRAME_DUMP_BUF_SIZE]; | |
| caption_frame_dump_buffer (frame, buff); | |
| fprintf (stderr, "%s\n", buff); | |
| } |