blob: 4bfe9cfe750ccdcc94c1a61d50b325f828b585b5
1 | /* |
2 | * Copyright (c) 2017 Paul B Mahol |
3 | * |
4 | * This file is part of FFmpeg. |
5 | * |
6 | * FFmpeg is free software; you can redistribute it and/or |
7 | * modify it under the terms of the GNU Lesser General Public |
8 | * License as published by the Free Software Foundation; either |
9 | * version 2.1 of the License, or (at your option) any later version. |
10 | * |
11 | * FFmpeg is distributed in the hope that it will be useful, |
12 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
13 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU |
14 | * Lesser General Public License for more details. |
15 | * |
16 | * You should have received a copy of the GNU Lesser General Public |
17 | * License along with FFmpeg; if not, write to the Free Software |
18 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
19 | */ |
20 | |
21 | /** |
22 | * @file |
23 | * Filter for reading closed captioning data (EIA-608). |
24 | * See also https://en.wikipedia.org/wiki/EIA-608 |
25 | */ |
26 | |
27 | #include <string.h> |
28 | |
29 | #include "libavutil/internal.h" |
30 | #include "libavutil/opt.h" |
31 | #include "libavutil/pixdesc.h" |
32 | #include "libavutil/timestamp.h" |
33 | |
34 | #include "avfilter.h" |
35 | #include "formats.h" |
36 | #include "internal.h" |
37 | #include "video.h" |
38 | |
39 | #define FALL 0 |
40 | #define RISE 1 |
41 | |
42 | typedef struct ReadEIA608Context { |
43 | const AVClass *class; |
44 | int start, end; |
45 | int min_range; |
46 | int max_peak_diff; |
47 | int max_period_diff; |
48 | int max_start_diff; |
49 | int nb_found; |
50 | int white; |
51 | int black; |
52 | float mpd, mhd, msd, mac, spw, bhd, wth, bth; |
53 | int chp; |
54 | } ReadEIA608Context; |
55 | |
56 | #define OFFSET(x) offsetof(ReadEIA608Context, x) |
57 | #define FLAGS AV_OPT_FLAG_VIDEO_PARAM|AV_OPT_FLAG_FILTERING_PARAM |
58 | |
59 | static const AVOption readeia608_options[] = { |
60 | { "scan_min", "set from which line to scan for codes", OFFSET(start), AV_OPT_TYPE_INT, {.i64=0}, 0, INT_MAX, FLAGS }, |
61 | { "scan_max", "set to which line to scan for codes", OFFSET(end), AV_OPT_TYPE_INT, {.i64=29}, 0, INT_MAX, FLAGS }, |
62 | { "mac", "set minimal acceptable amplitude change for sync codes detection", OFFSET(mac), AV_OPT_TYPE_FLOAT, {.dbl=.2}, 0.001, 1, FLAGS }, |
63 | { "spw", "set ratio of width reserved for sync code detection", OFFSET(spw), AV_OPT_TYPE_FLOAT, {.dbl=.27}, 0.1, 0.7, FLAGS }, |
64 | { "mhd", "set max peaks height difference for sync code detection", OFFSET(mhd), AV_OPT_TYPE_FLOAT, {.dbl=.1}, 0, 0.5, FLAGS }, |
65 | { "mpd", "set max peaks period difference for sync code detection", OFFSET(mpd), AV_OPT_TYPE_FLOAT, {.dbl=.1}, 0, 0.5, FLAGS }, |
66 | { "msd", "set first two max start code bits differences", OFFSET(msd), AV_OPT_TYPE_FLOAT, {.dbl=.02}, 0, 0.5, FLAGS }, |
67 | { "bhd", "set min ratio of bits height compared to 3rd start code bit", OFFSET(bhd), AV_OPT_TYPE_FLOAT, {.dbl=.75}, 0.01, 1, FLAGS }, |
68 | { "th_w", "set white color threshold", OFFSET(wth), AV_OPT_TYPE_FLOAT, {.dbl=.35}, 0.1, 1, FLAGS }, |
69 | { "th_b", "set black color threshold", OFFSET(bth), AV_OPT_TYPE_FLOAT, {.dbl=.15}, 0, 0.5, FLAGS }, |
70 | { "chp", "check and apply parity bit", OFFSET(chp), AV_OPT_TYPE_BOOL, {.i64= 0}, 0, 1, FLAGS }, |
71 | { NULL } |
72 | }; |
73 | |
74 | AVFILTER_DEFINE_CLASS(readeia608); |
75 | |
76 | static int query_formats(AVFilterContext *ctx) |
77 | { |
78 | static const enum AVPixelFormat pixel_fmts[] = { |
79 | AV_PIX_FMT_GRAY8, |
80 | AV_PIX_FMT_YUV410P, AV_PIX_FMT_YUV411P, |
81 | AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, |
82 | AV_PIX_FMT_YUV440P, AV_PIX_FMT_YUV444P, |
83 | AV_PIX_FMT_YUVJ420P, AV_PIX_FMT_YUVJ422P, |
84 | AV_PIX_FMT_YUVJ440P, AV_PIX_FMT_YUVJ444P, |
85 | AV_PIX_FMT_YUVJ411P, |
86 | AV_PIX_FMT_NONE |
87 | }; |
88 | AVFilterFormats *formats = ff_make_format_list(pixel_fmts); |
89 | if (!formats) |
90 | return AVERROR(ENOMEM); |
91 | return ff_set_common_formats(ctx, formats); |
92 | } |
93 | |
94 | static int config_input(AVFilterLink *inlink) |
95 | { |
96 | const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(inlink->format); |
97 | AVFilterContext *ctx = inlink->dst; |
98 | ReadEIA608Context *s = ctx->priv; |
99 | int depth = desc->comp[0].depth; |
100 | |
101 | if (s->end >= inlink->h) { |
102 | av_log(ctx, AV_LOG_WARNING, "Last line to scan too large, clipping.\n"); |
103 | s->end = inlink->h - 1; |
104 | } |
105 | |
106 | if (s->start > s->end) { |
107 | av_log(ctx, AV_LOG_ERROR, "Invalid range.\n"); |
108 | return AVERROR(EINVAL); |
109 | } |
110 | |
111 | s->min_range = s->mac * ((1 << depth) - 1); |
112 | s->max_peak_diff = s->mhd * ((1 << depth) - 1); |
113 | s->max_period_diff = s->mpd * ((1 << depth) - 1); |
114 | s->max_start_diff = s->msd * ((1 << depth) - 1); |
115 | s->white = s->wth * ((1 << depth) - 1); |
116 | s->black = s->bth * ((1 << depth) - 1); |
117 | |
118 | return 0; |
119 | } |
120 | |
121 | static void extract_line(AVFilterContext *ctx, AVFilterLink *inlink, AVFrame *in, int line) |
122 | { |
123 | ReadEIA608Context *s = ctx->priv; |
124 | int max = 0, min = INT_MAX; |
125 | int i, ch, range = 0; |
126 | const uint8_t *src; |
127 | uint16_t clock[8][2] = { { 0 } }; |
128 | const int sync_width = s->spw * in->width; |
129 | int last = 0, peaks = 0, max_peak_diff = 0, dir = RISE; |
130 | const int width_per_bit = (in->width - sync_width) / 19; |
131 | uint8_t byte[2] = { 0 }; |
132 | int s1, s2, s3, parity; |
133 | |
134 | src = &in->data[0][line * in->linesize[0]]; |
135 | for (i = 0; i < sync_width; i++) { |
136 | max = FFMAX(max, src[i]); |
137 | min = FFMIN(min, src[i]); |
138 | } |
139 | |
140 | range = max - min; |
141 | if (range < s->min_range) |
142 | return; |
143 | |
144 | for (i = 0; i < sync_width; i++) { |
145 | int Y = src[i]; |
146 | |
147 | if (dir == RISE) { |
148 | if (Y < last) { |
149 | dir = FALL; |
150 | if (last >= s->white) { |
151 | clock[peaks][0] = last; |
152 | clock[peaks][1] = i; |
153 | peaks++; |
154 | if (peaks > 7) |
155 | break; |
156 | } |
157 | } |
158 | } else if (dir == FALL) { |
159 | if (Y > last && last <= s->black) { |
160 | dir = RISE; |
161 | } |
162 | } |
163 | last = Y; |
164 | } |
165 | |
166 | if (peaks != 7) |
167 | return; |
168 | |
169 | for (i = 1; i < 7; i++) |
170 | max_peak_diff = FFMAX(max_peak_diff, FFABS(clock[i][0] - clock[i-1][0])); |
171 | |
172 | if (max_peak_diff > s->max_peak_diff) |
173 | return; |
174 | |
175 | max = 0; min = INT_MAX; |
176 | for (i = 1; i < 7; i++) { |
177 | max = FFMAX(max, FFABS(clock[i][1] - clock[i-1][1])); |
178 | min = FFMIN(min, FFABS(clock[i][1] - clock[i-1][1])); |
179 | } |
180 | |
181 | range = max - min; |
182 | if (range > s->max_period_diff) |
183 | return; |
184 | |
185 | s1 = src[sync_width + width_per_bit * 0 + width_per_bit / 2]; |
186 | s2 = src[sync_width + width_per_bit * 1 + width_per_bit / 2]; |
187 | s3 = src[sync_width + width_per_bit * 2 + width_per_bit / 2]; |
188 | |
189 | if (FFABS(s1 - s2) > s->max_start_diff || s1 > s->black || s2 > s->black || s3 < s->white) |
190 | return; |
191 | |
192 | for (ch = 0; ch < 2; ch++) { |
193 | for (parity = 0, i = 0; i < 8; i++) { |
194 | int b = src[sync_width + width_per_bit * (i + 3 + 8 * ch) + width_per_bit / 2]; |
195 | |
196 | if (b - s1 > (s3 - s1) * s->bhd) { |
197 | b = 1; |
198 | parity++; |
199 | } else { |
200 | b = 0; |
201 | } |
202 | byte[ch] |= b << i; |
203 | } |
204 | |
205 | if (s->chp) { |
206 | if (!(parity & 1)) { |
207 | byte[ch] = 0; |
208 | } |
209 | } |
210 | } |
211 | |
212 | { |
213 | uint8_t key[128], value[128]; |
214 | |
215 | snprintf(key, sizeof(key), "lavfi.readeia608.%d.cc", s->nb_found); |
216 | snprintf(value, sizeof(value), "0x%02X%02X", byte[0], byte[1]); |
217 | av_dict_set(avpriv_frame_get_metadatap(in), key, value, 0); |
218 | |
219 | snprintf(key, sizeof(key), "lavfi.readeia608.%d.line", s->nb_found); |
220 | snprintf(value, sizeof(value), "%d", line); |
221 | av_dict_set(avpriv_frame_get_metadatap(in), key, value, 0); |
222 | } |
223 | |
224 | s->nb_found++; |
225 | } |
226 | |
227 | static int filter_frame(AVFilterLink *inlink, AVFrame *in) |
228 | { |
229 | AVFilterContext *ctx = inlink->dst; |
230 | AVFilterLink *outlink = ctx->outputs[0]; |
231 | ReadEIA608Context *s = ctx->priv; |
232 | int i; |
233 | |
234 | s->nb_found = 0; |
235 | for (i = s->start; i <= s->end; i++) |
236 | extract_line(ctx, inlink, in, i); |
237 | |
238 | return ff_filter_frame(outlink, in); |
239 | } |
240 | |
241 | static const AVFilterPad readeia608_inputs[] = { |
242 | { |
243 | .name = "default", |
244 | .type = AVMEDIA_TYPE_VIDEO, |
245 | .filter_frame = filter_frame, |
246 | .config_props = config_input, |
247 | }, |
248 | { NULL } |
249 | }; |
250 | |
251 | static const AVFilterPad readeia608_outputs[] = { |
252 | { |
253 | .name = "default", |
254 | .type = AVMEDIA_TYPE_VIDEO, |
255 | }, |
256 | { NULL } |
257 | }; |
258 | |
259 | AVFilter ff_vf_readeia608 = { |
260 | .name = "readeia608", |
261 | .description = NULL_IF_CONFIG_SMALL("Read EIA-608 Closed Caption codes from input video and write them to frame metadata."), |
262 | .priv_size = sizeof(ReadEIA608Context), |
263 | .priv_class = &readeia608_class, |
264 | .query_formats = query_formats, |
265 | .inputs = readeia608_inputs, |
266 | .outputs = readeia608_outputs, |
267 | .flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC, |
268 | }; |
269 |