Reviewed-by: Derek Buitenhuis <derek.buitenhuis@gmail.com> Signed-off-by: Michael Niedermayer <michaelni@gmx.at>tags/n2.3
| @@ -32,6 +32,7 @@ version <next>: | |||
| - hqx filter (hq2x, hq3x, hq4x) | |||
| - flanger filter | |||
| - Image format auto-detection | |||
| - LRC demuxer and muxer | |||
| version 2.2: | |||
| @@ -310,6 +310,7 @@ library: | |||
| @tab Used by Linux Media Labs MPEG-4 PCI boards | |||
| @item LOAS @tab @tab X | |||
| @tab contains LATM multiplexed AAC audio | |||
| @item LRC @tab X @tab X | |||
| @item LVF @tab @tab X | |||
| @item LXF @tab @tab X | |||
| @tab VR native stream format, used by Leitch/Harris' video servers. | |||
| @@ -211,6 +211,8 @@ OBJS-$(CONFIG_LATM_DEMUXER) += rawdec.o | |||
| OBJS-$(CONFIG_LATM_MUXER) += latmenc.o rawenc.o | |||
| OBJS-$(CONFIG_LMLM4_DEMUXER) += lmlm4.o | |||
| OBJS-$(CONFIG_LOAS_DEMUXER) += loasdec.o rawdec.o | |||
| OBJS-$(CONFIG_LRC_DEMUXER) += lrcdec.o lrc.o subtitles.o | |||
| OBJS-$(CONFIG_LRC_MUXER) += lrcenc.o lrc.o | |||
| OBJS-$(CONFIG_LVF_DEMUXER) += lvfdec.o | |||
| OBJS-$(CONFIG_LXF_DEMUXER) += lxfdec.o | |||
| OBJS-$(CONFIG_M4V_DEMUXER) += m4vdec.o rawdec.o | |||
| @@ -163,6 +163,7 @@ void av_register_all(void) | |||
| REGISTER_MUXDEMUX(LATM, latm); | |||
| REGISTER_DEMUXER (LMLM4, lmlm4); | |||
| REGISTER_DEMUXER (LOAS, loas); | |||
| REGISTER_MUXDEMUX(LRC, lrc); | |||
| REGISTER_DEMUXER (LVF, lvf); | |||
| REGISTER_DEMUXER (LXF, lxf); | |||
| REGISTER_MUXDEMUX(M4V, m4v); | |||
| @@ -0,0 +1,34 @@ | |||
| /* | |||
| * LRC lyrics file format decoder | |||
| * Copyright (c) 2014 StarBrilliant <m13253@hotmail.com> | |||
| * | |||
| * This file is part of FFmpeg. | |||
| * | |||
| * FFmpeg is free software; you can redistribute it and/or | |||
| * modify it under the terms of the GNU Lesser General Public | |||
| * License as published by the Free Software Foundation; either | |||
| * version 2.1 of the License, or (at your option) any later version. | |||
| * | |||
| * FFmpeg is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
| * Lesser General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU Lesser General Public | |||
| * License along with FFmpeg; if not, write to the Free Software | |||
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |||
| */ | |||
| #include "metadata.h" | |||
| #include "lrc.h" | |||
| const AVMetadataConv ff_lrc_metadata_conv[] = { | |||
| {"ti", "title"}, | |||
| {"al", "album"}, | |||
| {"ar", "artist"}, | |||
| {"au", "author"}, | |||
| {"by", "creator"}, | |||
| {"re", "encoder"}, | |||
| {"ve", "encoder_version"}, | |||
| {0, 0} | |||
| }; | |||
| @@ -0,0 +1,29 @@ | |||
| /* | |||
| * LRC lyrics file format decoder | |||
| * Copyright (c) 2014 StarBrilliant <m13253@hotmail.com> | |||
| * | |||
| * This file is part of FFmpeg. | |||
| * | |||
| * FFmpeg is free software; you can redistribute it and/or | |||
| * modify it under the terms of the GNU Lesser General Public | |||
| * License as published by the Free Software Foundation; either | |||
| * version 2.1 of the License, or (at your option) any later version. | |||
| * | |||
| * FFmpeg is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
| * Lesser General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU Lesser General Public | |||
| * License along with FFmpeg; if not, write to the Free Software | |||
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |||
| */ | |||
| #ifndef AVFORMAT_LRC_H | |||
| #define AVFORMAT_LRC_H | |||
| #include "metadata.h" | |||
| extern const AVMetadataConv ff_lrc_metadata_conv[]; | |||
| #endif | |||
| @@ -0,0 +1,248 @@ | |||
| /* | |||
| * LRC lyrics file format decoder | |||
| * Copyright (c) 2014 StarBrilliant <m13253@hotmail.com> | |||
| * | |||
| * This file is part of FFmpeg. | |||
| * | |||
| * FFmpeg is free software; you can redistribute it and/or | |||
| * modify it under the terms of the GNU Lesser General Public | |||
| * License as published by the Free Software Foundation; either | |||
| * version 2.1 of the License, or (at your option) any later version. | |||
| * | |||
| * FFmpeg is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
| * Lesser General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU Lesser General Public | |||
| * License along with FFmpeg; if not, write to the Free Software | |||
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |||
| */ | |||
| #include <inttypes.h> | |||
| #include <stdint.h> | |||
| #include <string.h> | |||
| #include "avformat.h" | |||
| #include "internal.h" | |||
| #include "lrc.h" | |||
| #include "metadata.h" | |||
| #include "subtitles.h" | |||
| #include "libavutil/bprint.h" | |||
| #include "libavutil/dict.h" | |||
| typedef struct LRCContext { | |||
| FFDemuxSubtitlesQueue q; | |||
| int64_t ts_offset; // offset metadata item | |||
| } LRCContext; | |||
| static int64_t find_header(const char *p) | |||
| { | |||
| int64_t offset = 0; | |||
| while(p[offset] == ' ' || p[offset] == '\t') { | |||
| offset++; | |||
| } | |||
| if(p[offset] == '[' && p[offset + 1] >= 'a' && p[offset + 1] <= 'z') { | |||
| return offset; | |||
| } else { | |||
| return -1; | |||
| } | |||
| } | |||
| static int64_t count_ts(const char *p) | |||
| { | |||
| int64_t offset = 0; | |||
| int in_brackets = 0; | |||
| for(;;) { | |||
| if(p[offset] == ' ' || p[offset] == '\t') { | |||
| offset++; | |||
| } else if(p[offset] == '[') { | |||
| offset++; | |||
| in_brackets++; | |||
| } else if (p[offset] == ']' && in_brackets) { | |||
| offset++; | |||
| in_brackets--; | |||
| } else if(in_brackets && | |||
| (p[offset] == ':' || p[offset] == '.' || p[offset] == '-' || | |||
| (p[offset] >= '0' && p[offset] <= '9'))) { | |||
| offset++; | |||
| } else { | |||
| break; | |||
| } | |||
| } | |||
| return offset; | |||
| } | |||
| static int64_t read_ts(const char *p, int64_t *start) | |||
| { | |||
| int64_t offset = 0; | |||
| uint64_t mm, ss, cs; | |||
| while(p[offset] == ' ' || p[offset] == '\t') { | |||
| offset++; | |||
| } | |||
| if(p[offset] != '[') { | |||
| return 0; | |||
| } | |||
| if(sscanf(p, "[-%"SCNu64":%"SCNu64".%"SCNu64"]", &mm, &ss, &cs) == 3) { | |||
| /* Just in case negative pts, players may drop it but we won't. */ | |||
| *start = -(int64_t) (mm*60000 + ss*1000 + cs*10); | |||
| } else if(sscanf(p, "[%"SCNu64":%"SCNu64".%"SCNu64"]", &mm, &ss, &cs) == 3) { | |||
| *start = mm*60000 + ss*1000 + cs*10; | |||
| } else { | |||
| return 0; | |||
| } | |||
| do { | |||
| offset++; | |||
| } while(p[offset] && p[offset-1] != ']'); | |||
| return offset; | |||
| } | |||
| static int64_t read_line(AVBPrint *buf, AVIOContext *pb) | |||
| { | |||
| int64_t pos = avio_tell(pb); | |||
| av_bprint_clear(buf); | |||
| while(!url_feof(pb)) { | |||
| int c = avio_r8(pb); | |||
| if(c != '\r') { | |||
| av_bprint_chars(buf, c, 1); | |||
| } | |||
| if(c == '\n') { | |||
| break; | |||
| } | |||
| } | |||
| return pos; | |||
| } | |||
| static int lrc_probe(AVProbeData *p) | |||
| { | |||
| int64_t offset = 0; | |||
| int64_t mm; | |||
| uint64_t ss, cs; | |||
| const AVMetadataConv *metadata_item; | |||
| if(!memcmp(p->buf, "\xef\xbb\xbf", 3)) { // Skip UTF-8 BOM header | |||
| offset += 3; | |||
| } | |||
| while(p->buf[offset] == '\n' || p->buf[offset] == '\r') { | |||
| offset++; | |||
| } | |||
| if(p->buf[offset] != '[') { | |||
| return 0; | |||
| } | |||
| offset++; | |||
| // Common metadata item but not exist in ff_lrc_metadata_conv | |||
| if(!memcmp(p->buf + offset, "offset:", 7)) { | |||
| return 40; | |||
| } | |||
| if(sscanf(p->buf + offset, "%"SCNd64":%"SCNu64".%"SCNu64"]", | |||
| &mm, &ss, &cs) == 3) { | |||
| return 50; | |||
| } | |||
| // Metadata items exist in ff_lrc_metadata_conv | |||
| for(metadata_item = ff_lrc_metadata_conv; | |||
| metadata_item->native; metadata_item++) { | |||
| size_t metadata_item_len = strlen(metadata_item->native); | |||
| if(p->buf[offset + metadata_item_len] == ':' && | |||
| !memcmp(p->buf + offset, metadata_item->native, metadata_item_len)) { | |||
| return 40; | |||
| } | |||
| } | |||
| return 5; // Give it 5 scores since it starts with a bracket | |||
| } | |||
| static int lrc_read_header(AVFormatContext *s) | |||
| { | |||
| LRCContext *lrc = s->priv_data; | |||
| AVBPrint line; | |||
| AVStream *st; | |||
| st = avformat_new_stream(s, NULL); | |||
| if(!st) { | |||
| return AVERROR(ENOMEM); | |||
| } | |||
| avpriv_set_pts_info(st, 64, 1, 1000); | |||
| lrc->ts_offset = 0; | |||
| st->codec->codec_type = AVMEDIA_TYPE_SUBTITLE; | |||
| st->codec->codec_id = AV_CODEC_ID_TEXT; | |||
| av_bprint_init(&line, 0, AV_BPRINT_SIZE_UNLIMITED); | |||
| while(!url_feof(s->pb)) { | |||
| int64_t pos = read_line(&line, s->pb); | |||
| int64_t header_offset = find_header(line.str); | |||
| if(header_offset >= 0) { | |||
| char *comma_offset = strchr(line.str, ':'); | |||
| if(comma_offset) { | |||
| char *right_bracket_offset = strchr(line.str, ']'); | |||
| if(!right_bracket_offset) { | |||
| continue; | |||
| } | |||
| *right_bracket_offset = *comma_offset = '\0'; | |||
| if(strcmp(line.str + 1, "offset") || | |||
| sscanf(comma_offset + 1, "%"SCNd64, &lrc->ts_offset) != 1) { | |||
| av_dict_set(&s->metadata, line.str + 1, comma_offset + 1, 0); | |||
| } | |||
| *comma_offset = ':'; | |||
| *right_bracket_offset = ']'; | |||
| } | |||
| } else { | |||
| AVPacket *sub; | |||
| int64_t ts_start = AV_NOPTS_VALUE; | |||
| int64_t ts_stroffset = 0; | |||
| int64_t ts_stroffset_incr = 0; | |||
| int64_t ts_strlength = count_ts(line.str); | |||
| while((ts_stroffset_incr = read_ts(line.str + ts_stroffset, | |||
| &ts_start)) != 0) { | |||
| ts_stroffset += ts_stroffset_incr; | |||
| sub = ff_subtitles_queue_insert(&lrc->q, line.str + ts_strlength, | |||
| line.len - ts_strlength, 0); | |||
| if(!sub) { | |||
| return AVERROR(ENOMEM); | |||
| } | |||
| sub->pos = pos; | |||
| sub->pts = ts_start - lrc->ts_offset; | |||
| sub->duration = -1; | |||
| } | |||
| } | |||
| } | |||
| ff_subtitles_queue_finalize(&lrc->q); | |||
| ff_metadata_conv_ctx(s, NULL, ff_lrc_metadata_conv); | |||
| return 0; | |||
| } | |||
| static int lrc_read_packet(AVFormatContext *s, AVPacket *pkt) | |||
| { | |||
| LRCContext *lrc = s->priv_data; | |||
| return ff_subtitles_queue_read_packet(&lrc->q, pkt); | |||
| } | |||
| static int lrc_read_seek(AVFormatContext *s, int stream_index, | |||
| int64_t min_ts, int64_t ts, int64_t max_ts, int flags) | |||
| { | |||
| LRCContext *lrc = s->priv_data; | |||
| return ff_subtitles_queue_seek(&lrc->q, s, stream_index, | |||
| min_ts, ts, max_ts, flags); | |||
| } | |||
| static int lrc_read_close(AVFormatContext *s) | |||
| { | |||
| LRCContext *lrc = s->priv_data; | |||
| ff_subtitles_queue_clean(&lrc->q); | |||
| return 0; | |||
| } | |||
| AVInputFormat ff_lrc_demuxer = { | |||
| .name = "lrc", | |||
| .long_name = NULL_IF_CONFIG_SMALL("LRC lyrics"), | |||
| .priv_data_size = sizeof (LRCContext), | |||
| .read_probe = lrc_probe, | |||
| .read_header = lrc_read_header, | |||
| .read_packet = lrc_read_packet, | |||
| .read_close = lrc_read_close, | |||
| .read_seek2 = lrc_read_seek | |||
| }; | |||
| @@ -0,0 +1,152 @@ | |||
| /* | |||
| * LRC lyrics file format decoder | |||
| * Copyright (c) 2014 StarBrilliant <m13253@hotmail.com> | |||
| * | |||
| * This file is part of FFmpeg. | |||
| * | |||
| * FFmpeg is free software; you can redistribute it and/or | |||
| * modify it under the terms of the GNU Lesser General Public | |||
| * License as published by the Free Software Foundation; either | |||
| * version 2.1 of the License, or (at your option) any later version. | |||
| * | |||
| * FFmpeg is distributed in the hope that it will be useful, | |||
| * but WITHOUT ANY WARRANTY; without even the implied warranty of | |||
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |||
| * Lesser General Public License for more details. | |||
| * | |||
| * You should have received a copy of the GNU Lesser General Public | |||
| * License along with FFmpeg; if not, write to the Free Software | |||
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |||
| */ | |||
| #include <inttypes.h> | |||
| #include <stdint.h> | |||
| #include <string.h> | |||
| #include "avformat.h" | |||
| #include "internal.h" | |||
| #include "lrc.h" | |||
| #include "metadata.h" | |||
| #include "subtitles.h" | |||
| #include "version.h" | |||
| #include "libavutil/bprint.h" | |||
| #include "libavutil/dict.h" | |||
| #include "libavutil/log.h" | |||
| #include "libavutil/macros.h" | |||
| static int lrc_write_header(AVFormatContext *s) | |||
| { | |||
| const AVDictionaryEntry *metadata_item; | |||
| if(s->nb_streams != 1 || | |||
| s->streams[0]->codec->codec_type != AVMEDIA_TYPE_SUBTITLE) { | |||
| av_log(s, AV_LOG_ERROR, | |||
| "LRC supports only a single subtitle stream.\n"); | |||
| return AVERROR(EINVAL); | |||
| } | |||
| if(s->streams[0]->codec->codec_id != AV_CODEC_ID_SUBRIP && | |||
| s->streams[0]->codec->codec_id != AV_CODEC_ID_TEXT) { | |||
| av_log(s, AV_LOG_ERROR, "Unsupported subtitle codec: %s\n", | |||
| avcodec_get_name(s->streams[0]->codec->codec_id)); | |||
| return AVERROR(EINVAL); | |||
| } | |||
| avpriv_set_pts_info(s->streams[0], 64, 1, 100); | |||
| ff_metadata_conv_ctx(s, ff_lrc_metadata_conv, NULL); | |||
| if(!(s->flags & AVFMT_FLAG_BITEXACT)) { // avoid breaking regression tests | |||
| /* LRC provides a metadata slot for specifying encoder version | |||
| * in addition to encoder name. We will store LIBAVFORMAT_VERSION | |||
| * to it. | |||
| */ | |||
| av_dict_set(&s->metadata, "ve", AV_STRINGIFY(LIBAVFORMAT_VERSION), 0); | |||
| } else { | |||
| av_dict_set(&s->metadata, "ve", NULL, 0); | |||
| } | |||
| for(metadata_item = NULL; | |||
| (metadata_item = av_dict_get(s->metadata, "", metadata_item, | |||
| AV_DICT_IGNORE_SUFFIX)) != NULL;) { | |||
| char *delim; | |||
| if(!metadata_item->value[0]) { | |||
| continue; | |||
| } | |||
| while((delim = strchr(metadata_item->value, '\n')) != NULL) { | |||
| *delim = ' '; | |||
| } | |||
| while((delim = strchr(metadata_item->value, '\r')) != NULL) { | |||
| *delim = ' '; | |||
| } | |||
| avio_printf(s->pb, "[%s:%s]\n", | |||
| metadata_item->key, metadata_item->value); | |||
| } | |||
| avio_printf(s->pb, "\n"); | |||
| return 0; | |||
| } | |||
| static int lrc_write_packet(AVFormatContext *s, AVPacket *pkt) | |||
| { | |||
| if(pkt->pts != AV_NOPTS_VALUE) { | |||
| char *data = av_malloc(pkt->size + 1); | |||
| char *line; | |||
| char *delim; | |||
| if(!data) { | |||
| return AVERROR(ENOMEM); | |||
| } | |||
| memcpy(data, pkt->data, pkt->size); | |||
| data[pkt->size] = '\0'; | |||
| for(delim = data + pkt->size - 1; | |||
| delim >= data && (delim[0] == '\n' || delim[0] == '\r'); delim--) { | |||
| delim[0] = '\0'; // Strip last empty lines | |||
| } | |||
| line = data; | |||
| while(line[0] == '\n' || line[0] == '\r') { | |||
| line++; // Skip first empty lines | |||
| } | |||
| while(line) { | |||
| delim = strchr(line, '\n'); | |||
| if(delim) { | |||
| if(delim > line && delim[-1] == '\r') { | |||
| delim[-1] = '\0'; | |||
| } | |||
| delim[0] = '\0'; | |||
| delim++; | |||
| } | |||
| if(line[0] == '[') { | |||
| av_log(s, AV_LOG_WARNING, | |||
| "Subtitle starts with '[', may cause problems with LRC format.\n"); | |||
| } | |||
| if(pkt->pts >= 0) { | |||
| avio_printf(s->pb, "[%02"PRId64":%02"PRId64".%02"PRId64"]", | |||
| (pkt->pts / 6000), | |||
| ((pkt->pts / 100) % 60), | |||
| (pkt->pts % 100)); | |||
| } else { | |||
| /* Offset feature of LRC can easily make pts negative, | |||
| * we just output it directly and let the player drop it. */ | |||
| avio_printf(s->pb, "[-%02"PRId64":%02"PRId64".%02"PRId64"]", | |||
| (-pkt->pts) / 6000, | |||
| ((-pkt->pts) / 100) % 60, | |||
| (-pkt->pts) % 100); | |||
| } | |||
| avio_printf(s->pb, "%s\n", line); | |||
| line = delim; | |||
| } | |||
| av_free(data); | |||
| } | |||
| return 0; | |||
| } | |||
| AVOutputFormat ff_lrc_muxer = { | |||
| .name = "lrc", | |||
| .long_name = NULL_IF_CONFIG_SMALL("LRC lyrics"), | |||
| .extensions = "lrc", | |||
| .priv_data_size = 0, | |||
| .write_header = lrc_write_header, | |||
| .write_packet = lrc_write_packet, | |||
| .flags = AVFMT_VARIABLE_FPS | AVFMT_GLOBALHEADER | | |||
| AVFMT_TS_NEGATIVE | AVFMT_TS_NONSTRICT, | |||
| .subtitle_codec = AV_CODEC_ID_SUBRIP | |||
| }; | |||