├── License.txt
├── README.md
├── playback_standardcaptions.png
├── playback_zoomedin.png
└── sccyou
/License.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2019, Dave Rice
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # sccyou
2 | A script that interprets the results of FFmpeg's [readeia608](https://ffmpeg.org/ffmpeg-filters.html#readeia608) filter to create scc (and optionally srt) outputs from standard definition video files which contain [EIA-608](https://en.wikipedia.org/wiki/EIA-608) data.
3 |
4 | ## Installation
5 |
6 | ### macOS
7 |
8 | If you use macOS, you can easily install sccyou using a package manager called Homebrew. To install Homebrew, follow [these instructions](https://brew.sh/).
9 |
10 | To install sccyou, run the following two commands in a [Terminal window](https://en.wikipedia.org/wiki/Terminal_%28macOS%29):
11 | ```
12 | brew tap amiaopensource/amiaos
13 | brew install sccyou
14 | ```
15 | (The first taps the homebrew recipes of the amiaopensource account; the second installs sccyou and the other programs that it requires.)
16 |
17 | ## Dependencies
18 |
19 | ffmpeg 4.3 or later
20 |
21 | ## Usage
22 |
23 | To use sccyou, install it and run one of the following commands:
24 |
25 | ```
26 | sccyou -h display the help menu
27 | sccyou INPUT_FILE create an scc output
28 | sccyou -s INPUT_FILE create both scc and srt outputs
29 | sccyou -y INPUT_FILE overwrite any existing outputs
30 | sccyou -o DIRECTORY INPUT_FILE write output files to the provided directory (default is next to the input file)
31 | sccyou -l X INPUT_FILE read captions from line X (replace with a number) rather than let sccyou search for the captions
32 | ```
33 | The results of sccyou will vary based upon your specific command, but will look like sidecar `INPUT_FILE.scc` and `INPUT_FILE.srt` files.
34 |
35 | ## Playback
36 | After creating your sidecar files, sccyou will provide instructions for two forms of playback with captions (using `ffplay`).
37 |
38 | Standard playback, with the captioned text on screen:
39 | ```
40 | ffplay "INPUT_FILE" -vf subtitles="INPUT_FILE.scc"
41 | ```
42 |
43 | 
44 |
45 |
46 | Zoomed-in playback of caption data, scrolling vertically, with the captioned text appearing on screen:
47 |
48 | ```
49 | ffplay "INPUT_FILE" -vf format=rgb24,crop=iw:1:0:1,scale=iw:4:flags=neighbor,tile=layout=1x120:overlap=119:init_padding=119,setdar=4/3,subtitles="INPUT_FILE.scc"
50 | ```
51 |
52 | 
53 |
54 |
55 | ## Tell me more about closed captions
56 |
57 | Adobe's [Introduction to Closed Captions](https://www.adobe.com/content/dam/acom/en/devnet/video/pdfs/introduction_to_closed_captions.pdf) offers a good technical introduction to the various closed captioning formats that are out there.
58 | Please note: this script will only work with video files that contain EIA-608 (also known as CEA-608 or "line 21") captions, which were used exclusively in NTSC television broadcasts.
59 |
60 |
61 | More information about the scc format (hexadecimal character codes and sets) can be found [here](http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/).
62 |
63 |
64 | ## Feedback and Issues
65 | We welcome any feedback, in the [Issues](https://github.com/amiaopensource/sccyou/issues) section of this repo, though keep in mind that EIA-608 captions were often improperly created/duplicated, and ffmpeg can only retrieve that which isn't total trash.
66 |
67 |
68 | ## Code Contributors
69 | Dave Rice
70 |
71 | Paul Mahol
72 |
73 | ## Sponsors
74 | Development of sccyou was provided by New York Public Library's 2018/19 Innovation Project, and supported in part by the Charles H. Revson Foundation.
75 |
76 |
77 | ## Code of Conduct
78 |
79 | You can read our contributor code of conduct [here](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
80 |
81 | ## License
82 |
83 | sccyou is licensed under The MIT License.
84 |
--------------------------------------------------------------------------------
/playback_standardcaptions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amiaopensource/sccyou/bd6708f8c7fd5a867cfe5ca7a80024a744a19a61/playback_standardcaptions.png
--------------------------------------------------------------------------------
/playback_zoomedin.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/amiaopensource/sccyou/bd6708f8c7fd5a867cfe5ca7a80024a744a19a61/playback_zoomedin.png
--------------------------------------------------------------------------------
/sccyou:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # sccyou
3 | # script to apply the readeia608 filter and interpret the results in order to create an scc (and optionally srt or vtt) output from standard definition video files which contain EIA 608 data
4 | # thanks to Paul Mahol, Ben Turkus
5 | # Copyright (c) 2019-2024, Dave Rice, see License.txt
6 |
7 | VERSION="0.4"
8 |
9 | SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE=20
10 |
11 | SCRIPTDIR="$(dirname "${0}")"
12 | FFMPEG_INPUT_OPTIONS+=(-v 0)
13 | FFPROBE_INPUT_OPTIONS+=(-v 0)
14 |
15 | _usage(){
16 | cat < write output files to the provided directory (default is
25 | next to the input file)
26 | -s make an srt output as well as scc
27 | -w make a webvtt (vtt) output as well as scc
28 | -y overwrite any existing output files
29 | -v let ffmpeg be verbose, otherwise ffmpeg stderr is hidden
30 |
31 | Caption Discovery
32 | The location of the captions may either be specified as a line or a
33 | specified number of seconds may be searched to find captions. Note
34 | that searching for the caption line takes time, so providing the '-l'
35 | option to specify the line to use is faster.
36 |
37 | -l specify the line where caption data should be read,
38 | otherwise the first ${SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE} lines will be searched.
39 | -t specify an amount of time in seconds for which to search
40 | for captions, ${SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE} is used by default. By default the first 20
41 | seconds of the video is searched.
42 |
43 | Outcome
44 | will create MOVIE_FILE.scc and optionally MOVIE_FILE.srt or MOVIE_FILE.vtt
45 |
46 | Information about the scc format is available at http://www.theneitherworld.com/mcpoodle/SCC_TOOLS/DOCS/
47 |
48 | Dependencies: ffmpeg 4.3 or later
49 |
50 | -F (provide a custom ffmpeg path. ffprobe is presumed to neighbor ffmpeg)
51 |
52 | If an ffmpeg binary is provided with the '-F ' option that will be used,
53 | else if an ffmpeg binary exists within the same directory of sccyou, that will
54 | be used. Else it will use $(which ffmpeg).
55 |
56 | How tos:
57 | Playback movie with the scc file:
58 | ffplay MOVIE_FILE.mov -vf subtitles=MOVIE_FILE.scc
59 | EOF
60 | }
61 |
62 | _mkdir2(){
63 | local DIR2MAKE=""
64 | while [ "${*}" != "" ] ; do
65 | DIR2MAKE="${1}"
66 | if [ ! -d "${DIR2MAKE}" ] ; then
67 | mkdir -p "${DIR2MAKE}"
68 | if [ "${?}" -ne 0 ]; then
69 | _report -wt "${0}: Can't create directory at ${DIR2MAKE}"
70 | exit 1
71 | fi
72 | fi
73 | shift
74 | done
75 | }
76 |
77 | _report(){
78 | local RED="$(tput setaf 1)" # Red - For Warnings
79 | local GREEN="$(tput setaf 2)" # Green - For Declarations
80 | local BLUE="$(tput setaf 4)" # Blue - For Questions
81 | local NC="$(tput sgr0)" # No Color
82 | local COLOR=""
83 | local STARTMESSAGE=""
84 | local ENDMESSAGE=""
85 | local ECHOOPT=""
86 | local LOG_MESSAGE=""
87 | OPTIND=1
88 | while getopts ":qdwstn" OPT; do
89 | case "${OPT}" in
90 | q) COLOR="${BLUE}" ;;
91 | d) COLOR="${GREEN}" ;;
92 | w) COLOR="${RED}" ; LOG_MESSAGE="Y" ;;
93 | s) STARTMESSAGE+=([$(basename "${0}")] ) ;;
94 | t) STARTMESSAGE+=($(_get_iso8601) '- ' ) ;;
95 | n) ECHOOPT="-n" ;;
96 | esac
97 | done
98 | shift $(( OPTIND - 1 ))
99 | MESSAGE="${1}"
100 | echo "${ECHOOPT}" "${COLOR}${STARTMESSAGE[@]}${MESSAGE}${NC}"
101 | [ "${LOG_MESSAGE}" = "Y" ]
102 | }
103 |
104 | _hhmmssmmm_to_hhmmssff(){
105 | tc="$1"
106 | echo "$tc" | awk -F "[:.]" '{ printf "%02i:%02i:%02i:%02i\n", $1, $2, $3, substr($4,1,3)*(30/1000) }'
107 | }
108 |
109 | _get_line(){
110 | LINE_SELECT="$("${FFMPEG_PATH}" -i "${MOVIE}" -vf "readeia608=lp=1:spw=${SPW},metadata=mode=print:key=lavfi.readeia608.0.line" -t "${SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE}" -f null - 2>&1 | grep -o "lavfi.readeia608.0.line=[0-9]*" | cut -d= -f2 | sort | uniq -c | sort -n -r | head -n 1 | awk '{print $2}')"
111 | }
112 |
113 | _get_cc(){
114 | # this function tests for the presence of caption data on the specified line
115 | LINE="${1}"
116 | SPW=0.27
117 | AMOUNT_TO_REPORT=10
118 | CC_SELECT="$("${FFMPEG_PATH}" -i "${MOVIE}" -vf "format=rgb24,crop=iw:1:0:${LINE},readeia608=spw=${SPW},metadata=mode=print:key=lavfi.readeia608.0.cc" -t "${SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE}" -f null - 2>&1 | grep -o "lavfi.readeia608.0.cc=0x[A-F0-9]*" | cut -d= -f2 | head -n "${AMOUNT_TO_REPORT}" | xargs)"
119 | }
120 |
121 | _get_caption_settings(){
122 | # test ratio of width reserved for sync code detection at both 0.3 and 0.27
123 | SPW="0.30"
124 | if [[ -n "${LINE_SELECT_USER}" ]] ; then
125 | _get_cc "${LINE_SELECT_USER}"
126 | if [[ -z "${CC_SELECT}" ]] ; then
127 | SPW="0.27"
128 | _get_cc "${LINE_SELECT_USER}"
129 | fi
130 | if [[ -z "${CC_SELECT}" ]] ; then
131 | _report -w "Sadly no caption data is detected in the first ${SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE} seconds of ${MOVIE} on line ${LINE_SELECT_USER}."
132 | _report -w "Try increasing the search time with '-t' or try another line with '-l'."
133 | break
134 | else
135 | LINE_SELECT="${LINE_SELECT_USER}"
136 | fi
137 | else
138 | _get_line "${MOVIE}"
139 | if [[ -z "${LINE_SELECT}" ]] ; then
140 | SPW="0.27"
141 | _get_line "${MOVIE}"
142 | fi
143 | if [[ -z "${LINE_SELECT}" ]] ; then
144 | _report -w "No caption data detected in the first ${SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE} seconds of ${MOVIE}."
145 | _report -w "Try increasing search time with '-t'."
146 | break
147 | fi
148 | fi
149 | _report -d "Found captions in ${MOVIE}, using spw=${SPW} and line ${LINE_SELECT}."
150 | }
151 |
152 | _write_cc(){
153 | CC="$1"
154 | if [[ "$START" == "Y" ]] ; then
155 | echo -n " ${CC}" >> "${SCC_OUT}"
156 | else
157 | echo -n " ${CC}" >> "${SCC_OUT}"
158 | fi
159 | START="N"
160 | }
161 |
162 | _write_new_cc_line(){
163 | SCC_TIME="$(_hhmmssmmm_to_hhmmssff "${SECS}")"
164 | echo -e -n "\n\n${SCC_TIME}" >> "${SCC_OUT}"
165 | START="Y"
166 | }
167 |
168 | _start_scc(){
169 | echo -n "Scenarist_SCC V1.0" > "${SCC_OUT}"
170 | }
171 |
172 | _end_scc(){
173 | echo >> "${SCC_OUT}"
174 | }
175 |
176 | while getopts ":hswyo:l:t:vF:" OPT ; do
177 | case "${OPT}" in
178 | h) _usage ; exit 0 ;;
179 | s) SRT_OUTPUT="y" ;;
180 | w) VTT_OUTPUT="y" ;;
181 | y) FFMPEG_INPUT_OPTIONS+=(-y) ; OVERWRITE="sure" ;;
182 | o) OUTPUT_DIR="${OPTARG}" ;;
183 | l) LINE_SELECT_USER="${OPTARG}" ;;
184 | t) SEARCH_FIRST_X_SECONDS_FOR_CAPTION_LINE="${OPTARG}" ;;
185 | v) unset FFMPEG_INPUT_OPTIONS ; unset FFPROBE_INPUT_OPTIONS ;;
186 | F) FFMPEG_PATH="${OPTARG}" ;;
187 | *) echo "bad option -${OPTARG}" ; _usage ; exit 1 ;;
188 | esac
189 | done
190 | shift $(( OPTIND - 1 ))
191 |
192 | # if FFMPEG_PATH wasn't provided as an argument then see if one is installed
193 | if [[ -z "${FFMPEG_PATH}" ]] ; then
194 | FFMPEG_GUESS="${SCRIPTDIR}/ffmpeg"
195 | if [[ -x "${FFMPEG_GUESS}" ]] ; then
196 | FFMPEG_PATH="${FFMPEG_GUESS}"
197 | else
198 | FFMPEG_PATH="$(which ffmpeg)"
199 | fi
200 | fi
201 | FFPROBE_GUESS="$(dirname "${FFMPEG_PATH}")/ffprobe"
202 | if [[ -x "${FFPROBE_GUESS}" ]] ; then
203 | FFPROBE_PATH="${FFPROBE_GUESS}"
204 | else
205 | FFPROBE_PATH="$(which ffprobe)"
206 | fi
207 |
208 | FFMPEG_INPUT_OPTIONS+=(-hide_banner)
209 | FFPROBE_INPUT_OPTIONS+=(-hide_banner)
210 |
211 | if [[ "${*}" == "" ]] ; then
212 | _usage
213 | exit
214 | fi
215 |
216 | while [[ "${*}" != "" ]] ; do
217 | MOVIE="${1}"
218 | if [[ -n "${OUTPUT_DIR}" ]] ; then
219 | MOVIE_BN="$(basename "${MOVIE}")"
220 | SCC_OUT="${OUTPUT_DIR}/${MOVIE_BN%.*}.scc"
221 | SRT_OUT="${OUTPUT_DIR}/${MOVIE_BN%.*}.srt"
222 | VTT_OUT="${OUTPUT_DIR}/${MOVIE_BN%.*}.vtt"
223 | if [[ ! -d "${OUTPUT_DIR}" ]] ; then
224 | _mkdir2 "${OUTPUT_DIR}"
225 | fi
226 | else
227 | SCC_OUT="${MOVIE%.*}.scc"
228 | SRT_OUT="${MOVIE%.*}.srt"
229 | VTT_OUT="${MOVIE%.*}.vtt"
230 | fi
231 | shift
232 |
233 | if [[ "${OVERWRITE}" != "sure" ]] ; then
234 | if [[ -f "${SCC_OUT}" ]] ; then
235 | _report -w "${SCC_OUT} already exists"
236 | break
237 | fi
238 | if [[ -f "${SRT_OUT}" ]] && [[ "${SRT_OUTPUT}" == "y" ]] ; then
239 | _report -w "${SRT_OUT} already exists"
240 | break
241 | fi
242 | if [[ -f "${VTT_OUT}" ]] && [[ "${VTT_OUTPUT}" == "y" ]] ; then
243 | _report -w "${VTT_OUT} already exists"
244 | break
245 | fi
246 | fi
247 |
248 | _get_caption_settings "${MOVIE}"
249 | _start_scc
250 |
251 | _report -d "Working on ${MOVIE}, preview with:"
252 | _report -q " ffplay \"${MOVIE}\" -vf subtitles=\"${SCC_OUT}\""
253 |
254 | PREV_CC="8080"
255 | CC_PRESENT=0
256 | while IFS="," read FRAME SECS CC_HEX ; do
257 | if [[ -n "${CC_HEX}" ]] ; then
258 | CC="${CC_HEX:2:4}"
259 | if { [[ "${CC}" != "8080" ]] && [[ "${PREV_CC}" == "8080" ]] ; } || { [[ "${CC}" == "9420" ]] && [[ "${PREV_CC}" != "9420" ]] ; } ; then
260 | _write_new_cc_line
261 | _write_cc "$CC"
262 | elif [[ "${CC}" != "8080" ]] ; then
263 | _write_cc "$CC"
264 | fi
265 | PREV_CC="$CC"
266 | if [[ "$CC_PRESENT" == 0 ]] ; then
267 | CC_PRESENT=1
268 | if [[ -n "${CC_LOG}" ]] ; then
269 | TRANSITION_TIME="$(_hhmmssmmm_to_hhmmssff "${SECS}")"
270 | _report -d "${CC_LOG} - ${TRANSITION_TIME}."
271 | fi
272 | TRANSITION_TIME="$(_hhmmssmmm_to_hhmmssff "${SECS}")"
273 | CC_LOG="Caption data ON: ${TRANSITION_TIME}"
274 | fi
275 | else
276 | if [[ "$CC_PRESENT" == 1 ]] ; then
277 | CC_PRESENT=0
278 | if [[ -n "${CC_LOG}" ]] ; then
279 | TRANSITION_TIME="$(_hhmmssmmm_to_hhmmssff "${SECS}")"
280 | _report -d "${CC_LOG} - ${TRANSITION_TIME}."
281 | fi
282 | TRANSITION_TIME="$(_hhmmssmmm_to_hhmmssff "${SECS}")"
283 | CC_LOG="Caption data OFF: ${TRANSITION_TIME}"
284 | fi
285 | PREV_CC="8080"
286 | fi
287 | PREV_SECS="$SECS"
288 | done < <("${FFPROBE_PATH}" "${FFPROBE_INPUT_OPTIONS[@]}" -sexagesimal -f lavfi -i movie="${MOVIE},format=rgb24,crop=iw:1:0:${LINE_SELECT},readeia608=lp=1:spw=${SPW}" -show_entries frame=best_effort_timestamp_time:frame_tags=lavfi.readeia608.0.cc -of csv)
289 | TRANSITION_TIME="$(_hhmmssmmm_to_hhmmssff "${PREV_SECS}")"
290 | _report -d "${CC_LOG} - ${TRANSITION_TIME}."
291 | _end_scc
292 |
293 | # Convert SCC to SRT if requested
294 | if [[ "${SRT_OUTPUT}" == "y" ]] ; then
295 | "${FFMPEG_PATH}" "${FFMPEG_INPUT_OPTIONS[@]}" -i "${SCC_OUT}" "${SRT_OUT}"
296 | fi
297 |
298 | # Convert SCC to WebVTT if requested
299 | if [[ "${VTT_OUTPUT}" == "y" ]] ; then
300 | "${FFMPEG_PATH}" "${FFMPEG_INPUT_OPTIONS[@]}" -i "${SCC_OUT}" "${VTT_OUT}"
301 | fi
302 |
303 | _report -d "Done with ${MOVIE}. We hope you like ${SCC_OUT}."
304 | done
305 |
--------------------------------------------------------------------------------