diff options
Diffstat (limited to 'mpvcore')
67 files changed, 25203 insertions, 0 deletions
diff --git a/mpvcore/asxparser.c b/mpvcore/asxparser.c new file mode 100644 index 0000000000..ec15313547 --- /dev/null +++ b/mpvcore/asxparser.c @@ -0,0 +1,590 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <stdarg.h> +#include <string.h> +#include <unistd.h> + +#include "playlist.h" +#include "playlist_parser.h" +#include "stream/stream.h" +#include "asxparser.h" +#include "core/mp_msg.h" + + +typedef struct ASX_Parser_t ASX_Parser_t; + +typedef struct { + char* buffer; + int line; +} ASX_LineSave_t; + +struct ASX_Parser_t { + int line; // Curent line + ASX_LineSave_t *ret_stack; + int ret_stack_size; + char* last_body; + int deep; + struct playlist *pl; +}; + +ASX_Parser_t *asx_parser_new(struct playlist *pl); + +void +asx_parser_free(ASX_Parser_t* parser); + +/* + * Return -1 on error, 0 when nothing is found, 1 on sucess + */ +int +asx_get_element(ASX_Parser_t* parser,char** _buffer, + char** _element,char** _body,char*** _attribs); + +int +asx_parse_attribs(ASX_Parser_t* parser,char* buffer,char*** _attribs); + +/////// Attribs utils + +char* +asx_get_attrib(const char* attrib,char** attribs); + +#define asx_free_attribs(a) asx_list_free(&a,free) + +////// List utils + +typedef void (*ASX_FreeFunc)(void* arg); + +void +asx_list_free(void* list_ptr,ASX_FreeFunc free_func); + + +////// List utils + +void +asx_list_free(void* list_ptr,ASX_FreeFunc free_func) { + void** ptr = *(void***)list_ptr; + if(ptr == NULL) return; + if(free_func != NULL) { + for( ; *ptr != NULL ; ptr++) + free_func(*ptr); + } + free(*(void**)list_ptr); + *(void**)list_ptr = NULL; +} + +/////// Attribs utils + +char* +asx_get_attrib(const char* attrib,char** attribs) { + char** ptr; + + if(attrib == NULL || attribs == NULL) return NULL; + for(ptr = attribs; ptr[0] != NULL; ptr += 2){ + if(strcasecmp(ptr[0],attrib) == 0) + return strdup(ptr[1]); + } + return NULL; +} + +#define asx_warning_attrib_required(p,e,a) mp_msg(MSGT_PLAYTREE,MSGL_WARN,"At line %d : element %s don't have the required attribute %s",p->line,e,a) +#define asx_warning_body_parse_error(p,e) mp_msg(MSGT_PLAYTREE,MSGL_WARN,"At line %d : error while parsing %s body",p->line,e) + +ASX_Parser_t *asx_parser_new(struct playlist *pl) +{ + ASX_Parser_t* parser = calloc(1,sizeof(ASX_Parser_t)); + parser->pl = pl; + return parser; +} + +void +asx_parser_free(ASX_Parser_t* parser) { + if(!parser) return; + free(parser->ret_stack); + free(parser); + +} + +#define LETTER "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +#define SPACE " \n\t\r" + +int +asx_parse_attribs(ASX_Parser_t* parser,char* buffer,char*** _attribs) { + char *ptr1, *ptr2, *ptr3; + int n_attrib = 0; + char **attribs = NULL; + char *attrib, *val; + + ptr1 = buffer; + while(1) { + for( ; strchr(SPACE,*ptr1) != NULL; ptr1++) { // Skip space + if(*ptr1 == '\0') break; + } + ptr3 = strchr(ptr1,'='); + if(ptr3 == NULL) break; + for(ptr2 = ptr3-1; strchr(SPACE,*ptr2) != NULL; ptr2--) { + if (ptr2 == ptr1) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : this should never append, back to attribute begin while skipping end space",parser->line); + break; + } + } + attrib = malloc(ptr2-ptr1+2); + strncpy(attrib,ptr1,ptr2-ptr1+1); + attrib[ptr2-ptr1+1] = '\0'; + + ptr1 = strchr(ptr3,'"'); + if(ptr1 == NULL || ptr1[1] == '\0') ptr1 = strchr(ptr3,'\''); + if(ptr1 == NULL || ptr1[1] == '\0') { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"At line %d : can't find attribute %s value",parser->line,attrib); + free(attrib); + break; + } + ptr2 = strchr(ptr1+1,ptr1[0]); + if (ptr2 == NULL) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"At line %d : value of attribute %s isn't finished",parser->line,attrib); + free(attrib); + break; + } + ptr1++; + val = malloc(ptr2-ptr1+1); + strncpy(val,ptr1,ptr2-ptr1); + val[ptr2-ptr1] = '\0'; + n_attrib++; + + attribs = realloc(attribs, (2 * n_attrib + 1) * sizeof(char*)); + attribs[n_attrib*2-2] = attrib; + attribs[n_attrib*2-1] = val; + + ptr1 = ptr2+1; + } + + if(n_attrib > 0) + attribs[n_attrib*2] = NULL; + + *_attribs = attribs; + + return n_attrib; +} + +/* + * Return -1 on error, 0 when nothing is found, 1 on sucess + */ +int +asx_get_element(ASX_Parser_t* parser,char** _buffer, + char** _element,char** _body,char*** _attribs) { + char *ptr1,*ptr2, *ptr3, *ptr4; + char *attribs = NULL; + char *element = NULL, *body = NULL, *ret = NULL, *buffer; + int n_attrib = 0; + int body_line = 0,attrib_line,ret_line,in = 0; + int quotes = 0; + + if(_buffer == NULL || _element == NULL || _body == NULL || _attribs == NULL) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : asx_get_element called with invalid value",parser->line); + return -1; + } + + *_body = *_element = NULL; + *_attribs = NULL; + buffer = *_buffer; + + if(buffer == NULL) return 0; + + if(parser->ret_stack && /*parser->last_body && */buffer != parser->last_body) { + ASX_LineSave_t* ls = parser->ret_stack; + int i; + for(i = 0 ; i < parser->ret_stack_size ; i++) { + if(buffer == ls[i].buffer) { + parser->line = ls[i].line; + break; + } + + } + if( i < parser->ret_stack_size) { + i++; + if( i < parser->ret_stack_size) + memmove(parser->ret_stack,parser->ret_stack+i, (parser->ret_stack_size - i)*sizeof(ASX_LineSave_t)); + parser->ret_stack_size -= i; + if(parser->ret_stack_size > 0) + parser->ret_stack = realloc(parser->ret_stack,parser->ret_stack_size*sizeof(ASX_LineSave_t)); + else { + free(parser->ret_stack); + parser->ret_stack = NULL; + } + } + } + + ptr1 = buffer; + while(1) { + for( ; ptr1[0] != '<' ; ptr1++) { + if(ptr1[0] == '\0') { + ptr1 = NULL; + break; + } + if(ptr1[0] == '\n') parser->line++; + } + //ptr1 = strchr(ptr1,'<'); + if(!ptr1 || ptr1[1] == '\0') return 0; // Nothing found + + if(strncmp(ptr1,"<!--",4) == 0) { // Comments + for( ; strncmp(ptr1,"-->",3) != 0 ; ptr1++) { + if(ptr1[0] == '\0') { + ptr1 = NULL; + break; + } + if(ptr1[0] == '\n') parser->line++; + } + //ptr1 = strstr(ptr1,"-->"); + if(!ptr1) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : unfinished comment",parser->line); + return -1; + } + } else { + break; + } + } + + // Is this space skip very useful ?? + for(ptr1++; strchr(SPACE,ptr1[0]) != NULL; ptr1++) { // Skip space + if(ptr1[0] == '\0') { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : EOB reached while parsing element start",parser->line); + return -1; + } + if(ptr1[0] == '\n') parser->line++; + } + + for(ptr2 = ptr1; strchr(LETTER,*ptr2) != NULL;ptr2++) { // Go to end of name + if(*ptr2 == '\0'){ + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : EOB reached while parsing element start",parser->line); + return -1; + } + if(ptr2[0] == '\n') parser->line++; + } + + element = malloc(ptr2-ptr1+1); + strncpy(element,ptr1,ptr2-ptr1); + element[ptr2-ptr1] = '\0'; + + for( ; strchr(SPACE,*ptr2) != NULL; ptr2++) { // Skip space + if(ptr2[0] == '\0') { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : EOB reached while parsing element start",parser->line); + free(element); + return -1; + } + if(ptr2[0] == '\n') parser->line++; + } + attrib_line = parser->line; + + + + for(ptr3 = ptr2; ptr3[0] != '\0'; ptr3++) { // Go to element end + if(ptr3[0] == '"') quotes ^= 1; + if(!quotes && (ptr3[0] == '>' || strncmp(ptr3,"/>",2) == 0)) + break; + if(ptr3[0] == '\n') parser->line++; + } + if(ptr3[0] == '\0' || ptr3[1] == '\0') { // End of file + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : EOB reached while parsing element start",parser->line); + free(element); + return -1; + } + + // Save attribs string + if(ptr3-ptr2 > 0) { + attribs = malloc(ptr3-ptr2+1); + strncpy(attribs,ptr2,ptr3-ptr2); + attribs[ptr3-ptr2] = '\0'; + } + //bs_line = parser->line; + if(ptr3[0] != '/') { // Not Self closed element + ptr3++; + for( ; strchr(SPACE,*ptr3) != NULL; ptr3++) { // Skip space on body begin + if(*ptr3 == '\0') { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : EOB reached while parsing %s element body",parser->line,element); + free(element); + free(attribs); + return -1; + } + if(ptr3[0] == '\n') parser->line++; + } + ptr4 = ptr3; + body_line = parser->line; + while(1) { // Find closing element + for( ; ptr4[0] != '<' ; ptr4++) { + if(ptr4[0] == '\0') { + ptr4 = NULL; + break; + } + if(ptr4[0] == '\n') parser->line++; + } + if(ptr4 && strncmp(ptr4,"<!--",4) == 0) { // Comments + for( ; strncmp(ptr4,"-->",3) != 0 ; ptr4++) { + if(ptr4[0] == '\0') { + ptr4 = NULL; + break; + } + if(ptr1[0] == '\n') parser->line++; + } + continue; + } + if(ptr4 == NULL || ptr4[1] == '\0') { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : EOB reached while parsing %s element body",parser->line,element); + free(element); + free(attribs); + return -1; + } + if(ptr4[1] != '/' && strncasecmp(element,ptr4+1,strlen(element)) == 0) { + in++; + ptr4+=2; + continue; + } else if(strncasecmp(element,ptr4+2,strlen(element)) == 0) { // Extract body + if(in > 0) { + in--; + ptr4 += 2+strlen(element); + continue; + } + ret = ptr4+strlen(element)+3; + if(ptr4 != ptr3) { + ptr4--; + for( ; ptr4 != ptr3 && strchr(SPACE,*ptr4) != NULL; ptr4--) ;// Skip space on body end + // if(ptr4[0] == '\0') parser->line--; + //} + ptr4++; + body = malloc(ptr4-ptr3+1); + strncpy(body,ptr3,ptr4-ptr3); + body[ptr4-ptr3] = '\0'; + } + break; + } else { + ptr4 += 2; + } + } + } else { + ret = ptr3 + 2; // 2 is for /> + } + + for( ; ret[0] != '\0' && strchr(SPACE,ret[0]) != NULL; ret++) { // Skip space + if(ret[0] == '\n') parser->line++; + } + + ret_line = parser->line; + + if(attribs) { + parser->line = attrib_line; + n_attrib = asx_parse_attribs(parser,attribs,_attribs); + free(attribs); + if(n_attrib < 0) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"At line %d : error while parsing element %s attributes",parser->line,element); + free(element); + free(body); + return -1; + } + } else + *_attribs = NULL; + + *_element = element; + *_body = body; + + parser->last_body = body; + parser->ret_stack_size++; + parser->ret_stack = realloc(parser->ret_stack,parser->ret_stack_size*sizeof(ASX_LineSave_t)); + if(parser->ret_stack_size > 1) + memmove(parser->ret_stack+1,parser->ret_stack,(parser->ret_stack_size-1)*sizeof(ASX_LineSave_t)); + parser->ret_stack[0].buffer = ret; + parser->ret_stack[0].line = ret_line; + parser->line = body ? body_line : ret_line; + + *_buffer = ret; + return 1; + +} + +static void +asx_parse_ref(ASX_Parser_t* parser, char** attribs) { + char *href; + + href = asx_get_attrib("HREF",attribs); + if(href == NULL) { + asx_warning_attrib_required(parser,"REF" ,"HREF" ); + return; + } +#if 0 + // replace http my mmshttp to avoid infinite loops + // disabled since some playlists for e.g. WinAMP use asx as well + // "-user-agent NSPlayer/4.1.0.3856" is a possible workaround + if (strncmp(href, "http://", 7) == 0) { + char *newref = malloc(3 + strlen(href) + 1); + strcpy(newref, "mms"); + strcpy(newref + 3, href); + free(href); + href = newref; + } +#endif + + playlist_add_file(parser->pl, href); + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Adding file %s to element entry\n",href); + + free(href); + +} + +static void asx_parse_entryref(ASX_Parser_t* parser,char* buffer,char** _attribs) { + char *href; + stream_t* stream; + + if(parser->deep > 0) + return; + + href = asx_get_attrib("HREF",_attribs); + if(href == NULL) { + asx_warning_attrib_required(parser,"ENTRYREF" ,"HREF" ); + return; + } + stream=stream_open(href, NULL); + if(!stream) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"Can't open playlist %s\n",href); + free(href); + return; + } + + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"Not recursively loading playlist %s\n",href); + + free_stream(stream); + free(href); + //mp_msg(MSGT_PLAYTREE,MSGL_INFO,"Need to implement entryref\n"); +} + +static void asx_parse_entry(ASX_Parser_t* parser,char* buffer,char** _attribs) { + char *element,*body,**attribs; + int r; + + while(buffer && buffer[0] != '\0') { + r = asx_get_element(parser,&buffer,&element,&body,&attribs); + if(r < 0) { + asx_warning_body_parse_error(parser,"ENTRY"); + return; + } else if (r == 0) { // No more element + break; + } + if(strcasecmp(element,"REF") == 0) { + asx_parse_ref(parser,attribs); + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Adding element %s to entry\n",element); + } else + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Ignoring element %s\n",element); + free(body); + asx_free_attribs(attribs); + } + +} + + +static void asx_parse_repeat(ASX_Parser_t* parser,char* buffer,char** _attribs) { + char *element,*body,**attribs; + int r; + + asx_get_attrib("COUNT",_attribs); + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"Ignoring repeated playlist entries\n"); + + while(buffer && buffer[0] != '\0') { + r = asx_get_element(parser,&buffer,&element,&body,&attribs); + if(r < 0) { + asx_warning_body_parse_error(parser,"REPEAT"); + return; + } else if (r == 0) { // No more element + break; + } + if(strcasecmp(element,"ENTRY") == 0) { + asx_parse_entry(parser,body,attribs); + } else if(strcasecmp(element,"ENTRYREF") == 0) { + asx_parse_entryref(parser,body,attribs); + } else if(strcasecmp(element,"REPEAT") == 0) { + asx_parse_repeat(parser,body,attribs); + } else + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Ignoring element %s\n",element); + free(body); + asx_free_attribs(attribs); + } + +} + + +bool asx_parse(char* buffer, struct playlist *pl) +{ + char *element,*asx_body,**asx_attribs,*body = NULL, **attribs; + int r; + ASX_Parser_t* parser = asx_parser_new(pl); + + parser->line = 1; + parser->deep = 0; + + r = asx_get_element(parser,&buffer,&element,&asx_body,&asx_attribs); + if(r < 0) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"At line %d : Syntax error ???",parser->line); + asx_parser_free(parser); + return false; + } else if(r == 0) { // No contents + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"empty asx element"); + asx_parser_free(parser); + return false; + } + + if(strcasecmp(element,"ASX") != 0) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"first element isn't ASX, it's %s\n",element); + asx_free_attribs(asx_attribs); + asx_parser_free(parser); + return false; + } + + if(!asx_body) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"ASX element is empty"); + asx_free_attribs(asx_attribs); + asx_parser_free(parser); + return false; + } + + buffer = asx_body; + while(buffer && buffer[0] != '\0') { + r = asx_get_element(parser,&buffer,&element,&body,&attribs); + if(r < 0) { + asx_warning_body_parse_error(parser,"ASX"); + asx_parser_free(parser); + return false; + } else if (r == 0) { // No more element + break; + } + if(strcasecmp(element,"ENTRY") == 0) { + asx_parse_entry(parser,body,attribs); + } else if(strcasecmp(element,"ENTRYREF") == 0) { + asx_parse_entryref(parser,body,attribs); + } else if(strcasecmp(element,"REPEAT") == 0) { + asx_parse_repeat(parser,body,attribs); + } else + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Ignoring element %s\n",element); + free(body); + asx_free_attribs(attribs); + } + + free(asx_body); + asx_free_attribs(asx_attribs); + asx_parser_free(parser); + return true; +} diff --git a/mpvcore/asxparser.h b/mpvcore/asxparser.h new file mode 100644 index 0000000000..e49a2cedc0 --- /dev/null +++ b/mpvcore/asxparser.h @@ -0,0 +1,27 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_ASXPARSER_H +#define MPLAYER_ASXPARSER_H + +#include <stdbool.h> + +struct playlist; +bool asx_parse(char* buffer, struct playlist *pl); + +#endif /* MPLAYER_ASXPARSER_H */ diff --git a/mpvcore/av_common.c b/mpvcore/av_common.c new file mode 100644 index 0000000000..a4dc525aa9 --- /dev/null +++ b/mpvcore/av_common.c @@ -0,0 +1,123 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> + +#include <libavutil/common.h> +#include <libavcodec/avcodec.h> + +#include "core/mp_talloc.h" +#include "demux/demux_packet.h" +#include "av_common.h" +#include "codecs.h" + + +// Copy the codec-related fields from st into avctx. This does not set the +// codec itself, only codec related header data provided by libavformat. +// The goal is to initialize a new decoder with the header data provided by +// libavformat, and unlike avcodec_copy_context(), allow the user to create +// a clean AVCodecContext for a manually selected AVCodec. +// This is strictly for decoding only. +void mp_copy_lav_codec_headers(AVCodecContext *avctx, AVCodecContext *st) +{ + if (st->extradata_size) { + av_free(avctx->extradata); + avctx->extradata_size = 0; + avctx->extradata = + av_mallocz(st->extradata_size + FF_INPUT_BUFFER_PADDING_SIZE); + if (avctx->extradata) { + avctx->extradata_size = st->extradata_size; + memcpy(avctx->extradata, st->extradata, st->extradata_size); + } + } + avctx->codec_tag = st->codec_tag; + avctx->stream_codec_tag = st->stream_codec_tag; + avctx->bit_rate = st->bit_rate; + avctx->width = st->width; + avctx->height = st->height; + avctx->pix_fmt = st->pix_fmt; + avctx->sample_aspect_ratio = st->sample_aspect_ratio; + avctx->chroma_sample_location = st->chroma_sample_location; + avctx->sample_rate = st->sample_rate; + avctx->channels = st->channels; + avctx->block_align = st->block_align; + avctx->channel_layout = st->channel_layout; + avctx->audio_service_type = st->audio_service_type; + avctx->bits_per_coded_sample = st->bits_per_coded_sample; +} + +// Set dst from mpkt. Note that dst is not refcountable. +// mpkt can be NULL to generate empty packets (used to flush delayed data). +// Does not set pts or duration fields. +void mp_set_av_packet(AVPacket *dst, struct demux_packet *mpkt) +{ + av_init_packet(dst); + dst->data = mpkt ? mpkt->buffer : NULL; + dst->size = mpkt ? mpkt->len : 0; + /* Some codecs (ZeroCodec, some cases of PNG) may want keyframe info + * from demuxer. */ + if (mpkt && mpkt->keyframe) + dst->flags |= AV_PKT_FLAG_KEY; + if (mpkt && mpkt->avpacket) { + dst->side_data = mpkt->avpacket->side_data; + dst->side_data_elems = mpkt->avpacket->side_data_elems; + } +} + +void mp_add_lavc_decoders(struct mp_decoder_list *list, enum AVMediaType type) +{ + AVCodec *cur = NULL; + for (;;) { + cur = av_codec_next(cur); + if (!cur) + break; + if (av_codec_is_decoder(cur) && cur->type == type) { + mp_add_decoder(list, "lavc", mp_codec_from_av_codec_id(cur->id), + cur->name, cur->long_name); + } + } +} + +int mp_codec_to_av_codec_id(const char *codec) +{ + int id = AV_CODEC_ID_NONE; + if (codec) { + const AVCodecDescriptor *desc = avcodec_descriptor_get_by_name(codec); + if (desc) + id = desc->id; + if (id == AV_CODEC_ID_NONE) { + AVCodec *avcodec = avcodec_find_decoder_by_name(codec); + if (avcodec) + id = avcodec->id; + } + } + return id; +} + +const char *mp_codec_from_av_codec_id(int codec_id) +{ + const char *name = NULL; + const AVCodecDescriptor *desc = avcodec_descriptor_get(codec_id); + if (desc) + name = desc->name; + if (!name) { + AVCodec *avcodec = avcodec_find_decoder(codec_id); + if (avcodec) + name = avcodec->name; + } + return name; +} diff --git a/mpvcore/av_common.h b/mpvcore/av_common.h new file mode 100644 index 0000000000..2fa8f127b0 --- /dev/null +++ b/mpvcore/av_common.h @@ -0,0 +1,33 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_AVCOMMON_H +#define MP_AVCOMMON_H + +#include <libavutil/avutil.h> +#include <libavcodec/avcodec.h> + +struct mp_decoder_list; +struct demux_packet; + +void mp_copy_lav_codec_headers(AVCodecContext *avctx, AVCodecContext *st); +void mp_set_av_packet(AVPacket *dst, struct demux_packet *mpkt); +void mp_add_lavc_decoders(struct mp_decoder_list *list, enum AVMediaType type); +int mp_codec_to_av_codec_id(const char *codec); +const char *mp_codec_from_av_codec_id(int codec_id); + +#endif diff --git a/mpvcore/av_log.c b/mpvcore/av_log.c new file mode 100644 index 0000000000..1331a1fb26 --- /dev/null +++ b/mpvcore/av_log.c @@ -0,0 +1,161 @@ +/* + * av_log to mp_msg converter + * Copyright (C) 2006 Michael Niedermayer + * Copyright (C) 2009 Uoti Urpala + * + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdlib.h> +#include <stdbool.h> + +#include "av_log.h" +#include "config.h" +#include "core/mp_msg.h" +#include <libavutil/avutil.h> +#include <libavutil/log.h> + +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libswscale/swscale.h> + +#ifdef CONFIG_LIBAVDEVICE +#include <libavdevice/avdevice.h> +#endif + +#ifdef CONFIG_LIBAVFILTER +#include <libavfilter/avfilter.h> +#endif + +static int av_log_level_to_mp_level(int av_level) +{ + if (av_level > AV_LOG_VERBOSE) + return MSGL_DBG2; + if (av_level > AV_LOG_INFO) + return MSGL_V; + if (av_level > AV_LOG_WARNING) + return MSGL_INFO; + if (av_level > AV_LOG_ERROR) + return MSGL_WARN; + if (av_level > AV_LOG_FATAL) + return MSGL_ERR; + return MSGL_FATAL; +} + +static int extract_msg_type_from_ctx(void *ptr) +{ + if (!ptr) + return MSGT_FIXME; + + AVClass *avc = *(AVClass **)ptr; + if (!avc) { + mp_msg(MSGT_FIXME, MSGL_WARN, + "av_log callback called with bad parameters (NULL AVClass).\n" + "This is a bug in one of Libav/FFmpeg libraries used.\n"); + return MSGT_FIXME; + } + + if (!strcmp(avc->class_name, "AVCodecContext")) { + AVCodecContext *s = ptr; + if (s->codec) { + if (s->codec->type == AVMEDIA_TYPE_AUDIO) { + if (s->codec->decode) + return MSGT_DECAUDIO; + } else if (s->codec->type == AVMEDIA_TYPE_VIDEO) { + if (s->codec->decode) + return MSGT_DECVIDEO; + } + // FIXME subtitles, encoders + // What msgt for them? There is nothing appropriate... + } + return MSGT_FIXME; + } + + if (!strcmp(avc->class_name, "AVFormatContext")) { + AVFormatContext *s = ptr; + if (s->iformat) + return MSGT_DEMUXER; + else if (s->oformat) + return MSGT_MUXER; + return MSGT_FIXME; + } + + return MSGT_FIXME; +} + +#if LIBAVCODEC_VERSION_MICRO >= 100 +#define LIB_PREFIX "ffmpeg" +#else +#define LIB_PREFIX "libav" +#endif + +static bool print_prefix = true; + +static void mp_msg_av_log_callback(void *ptr, int level, const char *fmt, + va_list vl) +{ + AVClass *avc = ptr ? *(AVClass **)ptr : NULL; + int mp_level = av_log_level_to_mp_level(level); + int type = extract_msg_type_from_ctx(ptr); + + if (!mp_msg_test(type, mp_level)) + return; + + if (print_prefix) { + mp_msg(type, mp_level, "[%s/%s] ", LIB_PREFIX, + avc ? avc->item_name(ptr) : "?"); + } + print_prefix = fmt[strlen(fmt) - 1] == '\n'; + + mp_msg_va(type, mp_level, fmt, vl); +} + +void init_libav(void) +{ + av_log_set_callback(mp_msg_av_log_callback); + avcodec_register_all(); + av_register_all(); + avformat_network_init(); + +#ifdef CONFIG_LIBAVFILTER + avfilter_register_all(); +#endif +#ifdef CONFIG_LIBAVDEVICE + avdevice_register_all(); +#endif +} + +#define V(x) (x)>>16, (x)>>8 & 255, (x) & 255 +static void print_version(char *name, unsigned buildv, unsigned runv) +{ + + if (buildv == runv) + mp_msg(MSGT_CPLAYER, MSGL_V, "Compiled against %s version %d.%d.%d\n", + name, V(buildv)); + else + mp_msg(MSGT_CPLAYER, MSGL_V, "Compiled against %s version %d.%d.%d " + "(runtime %d.%d.%d)\n", name, V(buildv), V(runv)); +} +#undef V + +void print_libav_versions(void) +{ + print_version("libavutil", LIBAVUTIL_VERSION_INT, avutil_version()); + print_version("libavcodec", LIBAVCODEC_VERSION_INT, avcodec_version()); + print_version("libavformat", LIBAVFORMAT_VERSION_INT, avformat_version()); + print_version("libswscale", LIBSWSCALE_VERSION_INT, swscale_version()); +} diff --git a/mpvcore/av_log.h b/mpvcore/av_log.h new file mode 100644 index 0000000000..833a7af03b --- /dev/null +++ b/mpvcore/av_log.h @@ -0,0 +1,2 @@ +void init_libav(void); +void print_libav_versions(void); diff --git a/mpvcore/av_opts.c b/mpvcore/av_opts.c new file mode 100644 index 0000000000..777a1eec5a --- /dev/null +++ b/mpvcore/av_opts.c @@ -0,0 +1,55 @@ +/* + * AVOption parsing helper + * Copyright (C) 2008 Michael Niedermayer + * + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdlib.h> +#include <string.h> + +#include <libavutil/opt.h> + +#include "av_opts.h" + +int parse_avopts(void *v, char *str){ + char *start; + + if (!str) + return 0; + + start= str= strdup(str); + + while(str && *str){ + char *next_opt, *arg; + + next_opt= strchr(str, ','); + if(next_opt) *next_opt++= 0; + + arg = strchr(str, '='); + if(arg) *arg++= 0; + + if (av_opt_set(v, str, arg, AV_OPT_SEARCH_CHILDREN) < 0) { + free(start); + return -1; + } + str= next_opt; + } + + free(start); + return 0; +} diff --git a/mpvcore/av_opts.h b/mpvcore/av_opts.h new file mode 100644 index 0000000000..640443a352 --- /dev/null +++ b/mpvcore/av_opts.h @@ -0,0 +1,30 @@ +/* + * AVOption parsing helper + * Copyright (C) 2008 Michael Niedermayer + * + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_AV_OPTS_H +#define MPLAYER_AV_OPTS_H + +/** + * Parses str and sets AVOptions in v accordingly. + */ +int parse_avopts(void *v, char *str); + +#endif /* MPLAYER_AV_OPTS_H */ diff --git a/mpvcore/bstr.c b/mpvcore/bstr.c new file mode 100644 index 0000000000..16da0993ea --- /dev/null +++ b/mpvcore/bstr.c @@ -0,0 +1,314 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <string.h> +#include <assert.h> +#include <ctype.h> +#include <stdarg.h> + +#include <libavutil/common.h> + +#include "talloc.h" + +#include "core/bstr.h" + +int bstrcmp(struct bstr str1, struct bstr str2) +{ + int ret = memcmp(str1.start, str2.start, FFMIN(str1.len, str2.len)); + + if (!ret) { + if (str1.len == str2.len) + return 0; + else if (str1.len > str2.len) + return 1; + else + return -1; + } + return ret; +} + +int bstrcasecmp(struct bstr str1, struct bstr str2) +{ + int ret = strncasecmp(str1.start, str2.start, FFMIN(str1.len, str2.len)); + + if (!ret) { + if (str1.len == str2.len) + return 0; + else if (str1.len > str2.len) + return 1; + else + return -1; + } + return ret; +} + +int bstrchr(struct bstr str, int c) +{ + for (int i = 0; i < str.len; i++) + if (str.start[i] == c) + return i; + return -1; +} + +int bstrrchr(struct bstr str, int c) +{ + for (int i = str.len - 1; i >= 0; i--) + if (str.start[i] == c) + return i; + return -1; +} + +int bstrcspn(struct bstr str, const char *reject) +{ + int i; + for (i = 0; i < str.len; i++) + if (strchr(reject, str.start[i])) + break; + return i; +} + +int bstrspn(struct bstr str, const char *accept) +{ + int i; + for (i = 0; i < str.len; i++) + if (!strchr(accept, str.start[i])) + break; + return i; +} + +int bstr_find(struct bstr haystack, struct bstr needle) +{ + for (int i = 0; i < haystack.len; i++) + if (bstr_startswith(bstr_splice(haystack, i, haystack.len), needle)) + return i; + return -1; +} + +struct bstr bstr_lstrip(struct bstr str) +{ + while (str.len && isspace(*str.start)) { + str.start++; + str.len--; + } + return str; +} + +struct bstr bstr_strip(struct bstr str) +{ + str = bstr_lstrip(str); + while (str.len && isspace(str.start[str.len - 1])) + str.len--; + return str; +} + +struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest) +{ + int start; + for (start = 0; start < str.len; start++) + if (!strchr(sep, str.start[start])) + break; + str = bstr_cut(str, start); + int end = bstrcspn(str, sep); + if (rest) { + *rest = bstr_cut(str, end); + } + return bstr_splice(str, 0, end); +} + +// Unlike with bstr_split(), tok is a string, and not a set of char. +// If tok is in str, return true, and: concat(out_left, tok, out_right) == str +// Otherwise, return false, and set out_left==str, out_right=="" +bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right) +{ + bstr bsep = bstr0(tok); + int pos = bstr_find(str, bsep); + if (pos < 0) + pos = str.len; + *out_left = bstr_splice(str, 0, pos); + *out_right = bstr_cut(str, pos + bsep.len); + return pos != str.len; +} + +struct bstr bstr_splice(struct bstr str, int start, int end) +{ + if (start < 0) + start += str.len; + if (end < 0) + end += str.len; + end = FFMIN(end, str.len); + start = FFMAX(start, 0); + end = FFMAX(end, start); + str.start += start; + str.len = end - start; + return str; +} + +long long bstrtoll(struct bstr str, struct bstr *rest, int base) +{ + str = bstr_lstrip(str); + char buf[51]; + int len = FFMIN(str.len, 50); + memcpy(buf, str.start, len); + buf[len] = 0; + char *endptr; + long long r = strtoll(buf, &endptr, base); + if (rest) + *rest = bstr_cut(str, endptr - buf); + return r; +} + +double bstrtod(struct bstr str, struct bstr *rest) +{ + str = bstr_lstrip(str); + char buf[101]; + int len = FFMIN(str.len, 100); + memcpy(buf, str.start, len); + buf[len] = 0; + char *endptr; + double r = strtod(buf, &endptr); + if (rest) + *rest = bstr_cut(str, endptr - buf); + return r; +} + +struct bstr *bstr_splitlines(void *talloc_ctx, struct bstr str) +{ + if (str.len == 0) + return NULL; + int count = 0; + for (int i = 0; i < str.len; i++) + if (str.start[i] == '\n') + count++; + if (str.start[str.len - 1] != '\n') + count++; + struct bstr *r = talloc_array_ptrtype(talloc_ctx, r, count); + unsigned char *p = str.start; + for (int i = 0; i < count - 1; i++) { + r[i].start = p; + while (*p++ != '\n'); + r[i].len = p - r[i].start; + } + r[count - 1].start = p; + r[count - 1].len = str.start + str.len - p; + return r; +} + +struct bstr bstr_getline(struct bstr str, struct bstr *rest) +{ + int pos = bstrchr(str, '\n'); + if (pos < 0) + pos = str.len; + if (rest) + *rest = bstr_cut(str, pos + 1); + return bstr_splice(str, 0, pos + 1); +} + +struct bstr bstr_strip_linebreaks(struct bstr str) +{ + if (bstr_endswith0(str, "\r\n")) { + str = bstr_splice(str, 0, str.len - 2); + } else if (bstr_endswith0(str, "\n")) { + str = bstr_splice(str, 0, str.len - 1); + } + return str; +} + +bool bstr_eatstart(struct bstr *s, struct bstr prefix) +{ + if (!bstr_startswith(*s, prefix)) + return false; + *s = bstr_cut(*s, prefix.len); + return true; +} + +void bstr_lower(struct bstr str) +{ + for (int i = 0; i < str.len; i++) + str.start[i] = tolower(str.start[i]); +} + +int bstr_sscanf(struct bstr str, const char *format, ...) +{ + char *ptr = bstrdup0(NULL, str); + va_list va; + va_start(va, format); + int ret = vsscanf(ptr, format, va); + va_end(va); + talloc_free(ptr); + return ret; +} + +int bstr_parse_utf8_code_length(unsigned char b) +{ + if (b < 128) + return 1; + int bytes = 7 - av_log2(b ^ 255); + return (bytes >= 2 && bytes <= 4) ? bytes : -1; +} + +int bstr_decode_utf8(struct bstr s, struct bstr *out_next) +{ + if (s.len == 0) + return -1; + unsigned int codepoint = s.start[0]; + s.start++; s.len--; + if (codepoint >= 128) { + int bytes = bstr_parse_utf8_code_length(codepoint); + if (bytes < 0 || s.len < bytes - 1) + return -1; + codepoint &= 127 >> bytes; + for (int n = 1; n < bytes; n++) { + int tmp = s.start[0]; + if ((tmp & 0xC0) != 0x80) + return -1; + codepoint = (codepoint << 6) | (tmp & ~0xC0); + s.start++; s.len--; + } + } + if (out_next) + *out_next = s; + return codepoint; +} + +bool bstr_case_startswith(struct bstr s, struct bstr prefix) +{ + struct bstr start = bstr_splice(s, 0, prefix.len); + return start.len == prefix.len && bstrcasecmp(start, prefix) == 0; +} + +bool bstr_case_endswith(struct bstr s, struct bstr suffix) +{ + struct bstr end = bstr_cut(s, -suffix.len); + return end.len == suffix.len && bstrcasecmp(end, suffix) == 0; +} + +struct bstr bstr_strip_ext(struct bstr str) +{ + int dotpos = bstrrchr(str, '.'); + if (dotpos < 0) + return str; + return (struct bstr){str.start, dotpos}; +} + +struct bstr bstr_get_ext(struct bstr s) +{ + int dotpos = bstrrchr(s, '.'); + if (dotpos < 0) + return (struct bstr){NULL, 0}; + return bstr_splice(s, dotpos + 1, s.len); +} diff --git a/mpvcore/bstr.h b/mpvcore/bstr.h new file mode 100644 index 0000000000..ce9e029ea5 --- /dev/null +++ b/mpvcore/bstr.h @@ -0,0 +1,183 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_BSTR_H +#define MPLAYER_BSTR_H + +#include <stdint.h> +#include <stddef.h> +#include <string.h> +#include <stdbool.h> + +#include "talloc.h" + +/* NOTE: 'len' is size_t, but most string-handling functions below assume + * that input size has been sanity checked and len fits in an int. + */ +typedef struct bstr { + unsigned char *start; + size_t len; +} bstr; + +// If str.start is NULL, return NULL. +static inline char *bstrdup0(void *talloc_ctx, struct bstr str) +{ + return talloc_strndup(talloc_ctx, (char *)str.start, str.len); +} + +// Like bstrdup0(), but always return a valid C-string. +static inline char *bstrto0(void *talloc_ctx, struct bstr str) +{ + return str.start ? bstrdup0(talloc_ctx, str) : talloc_strdup(talloc_ctx, ""); +} + +// Return start = NULL iff that is true for the original. +static inline struct bstr bstrdup(void *talloc_ctx, struct bstr str) +{ + struct bstr r = { NULL, str.len }; + if (str.start) + r.start = (unsigned char *)talloc_memdup(talloc_ctx, str.start, str.len); + return r; +} + +static inline struct bstr bstr0(const char *s) +{ + return (struct bstr){(unsigned char *)s, s ? strlen(s) : 0}; +} + +int bstrcmp(struct bstr str1, struct bstr str2); +int bstrcasecmp(struct bstr str1, struct bstr str2); +int bstrchr(struct bstr str, int c); +int bstrrchr(struct bstr str, int c); +int bstrspn(struct bstr str, const char *accept); +int bstrcspn(struct bstr str, const char *reject); + +int bstr_find(struct bstr haystack, struct bstr needle); +struct bstr *bstr_splitlines(void *talloc_ctx, struct bstr str); +struct bstr bstr_lstrip(struct bstr str); +struct bstr bstr_strip(struct bstr str); +struct bstr bstr_split(struct bstr str, const char *sep, struct bstr *rest); +bool bstr_split_tok(bstr str, const char *tok, bstr *out_left, bstr *out_right); +struct bstr bstr_splice(struct bstr str, int start, int end); +long long bstrtoll(struct bstr str, struct bstr *rest, int base); +double bstrtod(struct bstr str, struct bstr *rest); +void bstr_lower(struct bstr str); +int bstr_sscanf(struct bstr str, const char *format, ...); + +// Decode the UTF-8 code point at the start of the string,, and return the +// character. +// After calling this function, *out_next will point to the next character. +// out_next can be NULL. +// On error, -1 is returned, and *out_next is not modified. +int bstr_decode_utf8(struct bstr str, struct bstr *out_next); + +// Return the length of the UTF-8 sequence that starts with the given byte. +// Given a string char *s, the next UTF-8 code point is to be expected at +// s + bstr_parse_utf8_code_length(s[0]) +// On error, -1 is returned. On success, it returns a value in the range [1, 4]. +int bstr_parse_utf8_code_length(unsigned char b); + +// Return the text before the next line break, and return it. Change *rest to +// point to the text following this line break. (rest can be NULL.) +// Line break characters are not stripped. +struct bstr bstr_getline(struct bstr str, struct bstr *rest); + +// Strip one trailing line break. This is intended for use with bstr_getline, +// and will remove the trailing \n or \r\n sequence. +struct bstr bstr_strip_linebreaks(struct bstr str); + +// If s starts with prefix, return true and return the rest of the string in s. +bool bstr_eatstart(struct bstr *s, struct bstr prefix); + +bool bstr_case_startswith(struct bstr s, struct bstr prefix); +bool bstr_case_endswith(struct bstr s, struct bstr suffix); +struct bstr bstr_strip_ext(struct bstr str); +struct bstr bstr_get_ext(struct bstr s); + +static inline struct bstr bstr_cut(struct bstr str, int n) +{ + if (n < 0) { + n += str.len; + if (n < 0) + n = 0; + } + if (((size_t)n) > str.len) + n = str.len; + return (struct bstr){str.start + n, str.len - n}; +} + +static inline bool bstr_startswith(struct bstr str, struct bstr prefix) +{ + if (str.len < prefix.len) + return false; + return !memcmp(str.start, prefix.start, prefix.len); +} + +static inline bool bstr_startswith0(struct bstr str, const char *prefix) +{ + return bstr_startswith(str, bstr0(prefix)); +} + +static inline bool bstr_endswith(struct bstr str, struct bstr suffix) +{ + if (str.len < suffix.len) + return false; + return !memcmp(str.start + str.len - suffix.len, suffix.start, suffix.len); +} + +static inline bool bstr_endswith0(struct bstr str, const char *suffix) +{ + return bstr_endswith(str, bstr0(suffix)); +} + +static inline int bstrcmp0(struct bstr str1, const char *str2) +{ + return bstrcmp(str1, bstr0(str2)); +} + +static inline bool bstr_equals(struct bstr str1, struct bstr str2) +{ + return bstrcmp(str1, str2) == 0; +} + +static inline bool bstr_equals0(struct bstr str1, const char *str2) +{ + return bstrcmp(str1, bstr0(str2)) == 0; +} + +static inline int bstrcasecmp0(struct bstr str1, const char *str2) +{ + return bstrcasecmp(str1, bstr0(str2)); +} + +static inline int bstr_find0(struct bstr haystack, const char *needle) +{ + return bstr_find(haystack, bstr0(needle)); +} + +static inline int bstr_eatstart0(struct bstr *s, const char *prefix) +{ + return bstr_eatstart(s, bstr0(prefix)); +} + +// create a pair (not single value!) for "%.*s" printf syntax +#define BSTR_P(bstr) (int)((bstr).len), (bstr).start + +#define WHITESPACE " \f\n\r\t\v" + +#endif /* MPLAYER_BSTR_H */ diff --git a/mpvcore/charset_conv.c b/mpvcore/charset_conv.c new file mode 100644 index 0000000000..680c8f83f9 --- /dev/null +++ b/mpvcore/charset_conv.c @@ -0,0 +1,266 @@ +/* + * This file is part of mpv. + * + * Based on code taken from libass (ISC license), which was originally part + * of MPlayer (GPL). + * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com> + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdlib.h> +#include <errno.h> +#include <assert.h> + +#include "config.h" + +#include "core/mp_msg.h" + +#ifdef CONFIG_ENCA +#include <enca.h> +#endif + +#ifdef CONFIG_LIBGUESS +#include <libguess.h> +#endif + +#ifdef CONFIG_ICONV +#include <iconv.h> +#endif + +#include "charset_conv.h" + +// Split the string on ':' into components. +// out_arr is at least max entries long. +// Return number of out_arr entries filled. +static int split_colon(const char *user_cp, int max, bstr *out_arr) +{ + if (!user_cp || max < 1) + return 0; + + int count = 0; + while (1) { + const char *next = strchr(user_cp, ':'); + if (next && max - count > 1) { + out_arr[count++] = (bstr){(char *)user_cp, next - user_cp}; + user_cp = next + 1; + } else { + out_arr[count++] = (bstr){(char *)user_cp, strlen(user_cp)}; + break; + } + } + return count; +} + +// Returns true if user_cp implies that calling mp_charset_guess() on the +// input data is required to determine the real codepage. This is the case +// if user_cp is not a real iconv codepage, but a magic value that requests +// for example ENCA charset auto-detection. +bool mp_charset_requires_guess(const char *user_cp) +{ + bstr res[2] = {{0}}; + split_colon(user_cp, 2, res); + return bstrcasecmp0(res[0], "enca") == 0 || + bstrcasecmp0(res[0], "guess") == 0; +} + +#ifdef CONFIG_ENCA +static const char *enca_guess(bstr buf, const char *language) +{ + if (!language || !language[0]) + language = "__"; // neutral language + + const char *detected_cp = NULL; + + EncaAnalyser analyser = enca_analyser_alloc(language); + if (analyser) { + enca_set_termination_strictness(analyser, 0); + EncaEncoding enc = enca_analyse_const(analyser, buf.start, buf.len); + const char *tmp = enca_charset_name(enc.charset, ENCA_NAME_STYLE_ICONV); + if (tmp && enc.charset != ENCA_CS_UNKNOWN) + detected_cp = tmp; + enca_analyser_free(analyser); + } else { + mp_msg(MSGT_SUBREADER, MSGL_ERR, "ENCA doesn't know language '%s'\n", + language); + size_t langcnt; + const char **languages = enca_get_languages(&langcnt); + mp_msg(MSGT_SUBREADER, MSGL_ERR, "ENCA supported languages:"); + for (int i = 0; i < langcnt; i++) + mp_msg(MSGT_SUBREADER, MSGL_ERR, " %s", languages[i]); + mp_msg(MSGT_SUBREADER, MSGL_ERR, "\n"); + free(languages); + } + + return detected_cp; +} +#endif + +#ifdef CONFIG_LIBGUESS +static const char *libguess_guess(bstr buf, const char *language) +{ + if (libguess_validate_utf8(buf.start, buf.len)) + return "UTF-8"; + + if (!language || !language[0] || strcmp(language, "help") == 0) { + mp_msg(MSGT_SUBREADER, MSGL_ERR, "libguess needs a language: " + "japanese taiwanese chinese korean russian arabic turkish " + "greek hebrew polish baltic\n"); + return NULL; + } + + return libguess_determine_encoding(buf.start, buf.len, language); +} +#endif + +// Runs charset auto-detection on the input buffer, and returns the result. +// If auto-detection fails, NULL is returned. +// If user_cp doesn't refer to any known auto-detection (for example because +// it's a real iconv codepage), user_cp is returned without even looking at +// the buf data. +const char *mp_charset_guess(bstr buf, const char *user_cp) +{ + if (!mp_charset_requires_guess(user_cp)) + return user_cp; + + bstr params[3] = {{0}}; + split_colon(user_cp, 3, params); + + bstr type = params[0]; + char lang[100]; + snprintf(lang, sizeof(lang), "%.*s", BSTR_P(params[1])); + const char *fallback = params[2].start; // last item, already 0-terminated + + const char *res = NULL; + +#ifdef CONFIG_ENCA + if (bstrcasecmp0(type, "enca") == 0) + res = enca_guess(buf, lang); +#endif +#ifdef CONFIG_LIBGUESS + if (bstrcasecmp0(type, "guess") == 0) + res = libguess_guess(buf, lang); +#endif + + if (res) { + mp_msg(MSGT_SUBREADER, MSGL_DBG2, "%.*s detected charset: '%s'\n", + BSTR_P(type), res); + } else { + res = fallback; + mp_msg(MSGT_SUBREADER, MSGL_DBG2, + "Detection with %.*s failed: fallback to %s\n", + BSTR_P(type), res && res[0] ? res : "no conversion"); + } + + return res; +} + +// Convert the data in buf to UTF-8. The charset argument can be an iconv +// codepage, a value returned by mp_charset_conv_guess(), or a special value +// that triggers autodetection of the charset (e.g. using ENCA). +// The auto-detection is the only difference to mp_iconv_to_utf8(). +// buf: same as mp_iconv_to_utf8() +// user_cp: iconv codepage, special value, NULL +// flags: same as mp_iconv_to_utf8() +// returns: same as mp_iconv_to_utf8() +bstr mp_charset_guess_and_conv_to_utf8(bstr buf, const char *user_cp, int flags) +{ + return mp_iconv_to_utf8(buf, mp_charset_guess(buf, user_cp), flags); +} + +// Use iconv to convert buf to UTF-8. +// Returns buf.start==NULL on error. Returns buf if cp is NULL, or if there is +// obviously no conversion required (e.g. if cp is "UTF-8"). +// Returns a newly allocated buffer if conversion is done and succeeds. The +// buffer will be terminated with 0 for convenience (the terminating 0 is not +// included in the returned length). +// Free the returned buffer with talloc_free(). +// buf: input data +// cp: iconv codepage (or NULL) +// flags: combination of MP_ICONV_* flags +// returns: buf (no conversion), .start==NULL (error), or allocated buffer +bstr mp_iconv_to_utf8(bstr buf, const char *cp, int flags) +{ +#ifdef CONFIG_ICONV + const char *tocp = "UTF-8"; + + if (!cp || !cp[0] || strcasecmp(cp, tocp) == 0) + return buf; + + if (strcasecmp(cp, "ASCII") == 0) + return buf; + + iconv_t icdsc; + if ((icdsc = iconv_open(tocp, cp)) == (iconv_t) (-1)) { + if (flags & MP_ICONV_VERBOSE) + mp_msg(MSGT_SUBREADER, MSGL_ERR, + "Error opening iconv with codepage '%s'\n", cp); + goto failure; + } + + size_t size = buf.len; + size_t osize = size; + size_t ileft = size; + size_t oleft = size - 1; + + char *outbuf = talloc_size(NULL, osize); + char *ip = buf.start; + char *op = outbuf; + + while (1) { + int clear = 0; + size_t rc; + if (ileft) + rc = iconv(icdsc, &ip, &ileft, &op, &oleft); + else { + clear = 1; // clear the conversion state and leave + rc = iconv(icdsc, NULL, NULL, &op, &oleft); + } + if (rc == (size_t) (-1)) { + if (errno == E2BIG) { + size_t offset = op - outbuf; + outbuf = talloc_realloc_size(NULL, outbuf, osize + size); + op = outbuf + offset; + osize += size; + oleft += size; + } else { + if (errno == EINVAL && (flags & MP_ICONV_ALLOW_CUTOFF)) { + // This is intended for cases where the input buffer is cut + // at a random byte position. If this happens in the middle + // of the buffer, it should still be an error. We say it's + // fine if the error is within 10 bytes of the end. + if (ileft <= 10) + break; + } + if (flags & MP_ICONV_VERBOSE) { + mp_msg(MSGT_SUBREADER, MSGL_ERR, + "Error recoding text with codepage '%s'\n", cp); + } + talloc_free(outbuf); + iconv_close(icdsc); + goto failure; + } + } else if (clear) + break; + } + + iconv_close(icdsc); + + outbuf[osize - oleft - 1] = 0; + return (bstr){outbuf, osize - oleft - 1}; +#endif + +failure: + return (bstr){0}; +} diff --git a/mpvcore/charset_conv.h b/mpvcore/charset_conv.h new file mode 100644 index 0000000000..00a2658da3 --- /dev/null +++ b/mpvcore/charset_conv.h @@ -0,0 +1,17 @@ +#ifndef MP_CHARSET_CONV_H +#define MP_CHARSET_CONV_H + +#include <stdbool.h> +#include "core/bstr.h" + +enum { + MP_ICONV_VERBOSE = 1, // print errors instead of failing silently + MP_ICONV_ALLOW_CUTOFF = 2, // allow partial input data +}; + +bool mp_charset_requires_guess(const char *user_cp); +const char *mp_charset_guess(bstr buf, const char *user_cp); +bstr mp_charset_guess_and_conv_to_utf8(bstr buf, const char *user_cp, int flags); +bstr mp_iconv_to_utf8(bstr buf, const char *cp, int flags); + +#endif diff --git a/mpvcore/codecs.c b/mpvcore/codecs.c new file mode 100644 index 0000000000..943860a70b --- /dev/null +++ b/mpvcore/codecs.c @@ -0,0 +1,147 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include "core/mp_talloc.h" +#include "core/bstr.h" +#include "core/mp_msg.h" +#include "codecs.h" + +void mp_add_decoder(struct mp_decoder_list *list, const char *family, + const char *codec, const char *decoder, const char *desc) +{ + struct mp_decoder_entry entry = { + .family = talloc_strdup(list, family), + .codec = talloc_strdup(list, codec), + .decoder = talloc_strdup(list, decoder), + .desc = talloc_strdup(list, desc), + }; + MP_TARRAY_APPEND(list, list->entries, list->num_entries, entry); +} + +static void mp_add_decoder_entry(struct mp_decoder_list *list, + struct mp_decoder_entry *entry) +{ + mp_add_decoder(list, entry->family, entry->codec, entry->decoder, + entry->desc); +} + +static struct mp_decoder_entry *find_decoder(struct mp_decoder_list *list, + bstr family, bstr decoder) +{ + for (int n = 0; n < list->num_entries; n++) { + struct mp_decoder_entry *cur = &list->entries[n]; + if (bstr_equals0(decoder, cur->decoder) && + bstr_equals0(family, cur->family)) + return cur; + } + return NULL; +} + +// Add entry, but only if it's not yet on the list, and if the codec matches. +// If codec == NULL, don't compare codecs. +static void add_new(struct mp_decoder_list *to, struct mp_decoder_entry *entry, + const char *codec) +{ + if (!entry || (codec && strcmp(entry->codec, codec) != 0)) + return; + if (!find_decoder(to, bstr0(entry->family), bstr0(entry->decoder))) + mp_add_decoder_entry(to, entry); +} + +// Select a decoder from the given list for the given codec. The selection +// can be influenced by the selection string, which can specify a priority +// list of preferred decoders. +// This returns a list of decoders to try, with the preferred decoders first. +// The selection string corresponds to --vd/--ad directly, and has the +// following syntax: +// selection = [<entry> ("," <entry>)*] +// entry = <family> ":" <decoder> // prefer decoder +// entry = <family> ":*" // prefer all decoders +// entry = "+" <family> ":" <decoder> // force a decoder +// entry = "-" <family> ":" <decoder> // exclude a decoder +// entry = "-" // don't add fallback decoders +// Forcing a decoder means it's added even if the codec mismatches. +struct mp_decoder_list *mp_select_decoders(struct mp_decoder_list *all, + const char *codec, + const char *selection) +{ + struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list); + struct mp_decoder_list *remove = talloc_zero(NULL, struct mp_decoder_list); + if (!codec) + codec = "unknown"; + bool stop = false; + bstr sel = bstr0(selection); + while (sel.len) { + bstr entry; + bstr_split_tok(sel, ",", &entry, &sel); + if (bstr_equals0(entry, "-")) { + stop = true; + break; + } + bool force = bstr_eatstart0(&entry, "+"); + bool exclude = !force && bstr_eatstart0(&entry, "-"); + struct mp_decoder_list *dest = exclude ? remove : list; + bstr family, decoder; + if (!bstr_split_tok(entry, ":", &family, &decoder)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Decoders must be specified as " + "'family:decoder' for the --ad/--vd options.\n"); + break; + } + if (bstr_equals0(decoder, "*")) { + for (int n = 0; n < all->num_entries; n++) { + struct mp_decoder_entry *cur = &all->entries[n]; + if (bstr_equals0(family, cur->family)) + add_new(dest, cur, codec); + } + } else { + add_new(dest, find_decoder(all, family, decoder), + force ? NULL : codec); + } + } + if (!stop) { + // Add the remaining codecs which haven't been added yet + for (int n = 0; n < all->num_entries; n++) + add_new(list, &all->entries[n], codec); + } + for (int n = 0; n < remove->num_entries; n++) { + struct mp_decoder_entry *ex = &remove->entries[n]; + struct mp_decoder_entry *del = + find_decoder(list, bstr0(ex->family), bstr0(ex->decoder)); + if (del) { + int index = del - &list->entries[0]; + MP_TARRAY_REMOVE_AT(list->entries, list->num_entries, index); + } + } + talloc_free(remove); + return list; +} + +void mp_print_decoders(int msgt, int msgl, const char *header, + struct mp_decoder_list *list) +{ + mp_msg(msgt, msgl, "%s\n", header); + for (int n = 0; n < list->num_entries; n++) { + struct mp_decoder_entry *entry = &list->entries[n]; + mp_msg(msgt, msgl, " %s:%s", entry->family, entry->decoder); + if (strcmp(entry->decoder, entry->codec) != 0) + mp_msg(msgt, msgl, " (%s)", entry->codec); + mp_msg(msgt, msgl, " - %s\n", entry->desc); + } + if (list->num_entries == 0) + mp_msg(msgt, msgl, " (no decoders)\n"); +} diff --git a/mpvcore/codecs.h b/mpvcore/codecs.h new file mode 100644 index 0000000000..21ff284617 --- /dev/null +++ b/mpvcore/codecs.h @@ -0,0 +1,43 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MP_CODECS_H +#define MP_CODECS_H + +struct mp_decoder_entry { + const char *family; // decoder module (e.g. ad_lavc => "lavc") + const char *codec; // name of the codec (e.g. "mp3") + const char *decoder; // decoder name (e.g. "mp3float") + const char *desc; // human readable description +}; + +struct mp_decoder_list { + struct mp_decoder_entry *entries; + int num_entries; +}; + +void mp_add_decoder(struct mp_decoder_list *list, const char *family, + const char *codec, const char *decoder, const char *desc); + +struct mp_decoder_list *mp_select_decoders(struct mp_decoder_list *all, + const char *codec, + const char *selection); + +void mp_print_decoders(int msgt, int msgl, const char *header, + struct mp_decoder_list *list); + +#endif diff --git a/mpvcore/command.c b/mpvcore/command.c new file mode 100644 index 0000000000..b7718e41b7 --- /dev/null +++ b/mpvcore/command.c @@ -0,0 +1,2673 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdlib.h> +#include <inttypes.h> +#include <unistd.h> +#include <string.h> +#include <stdbool.h> +#include <assert.h> +#include <time.h> + +#include <libavutil/avstring.h> +#include <libavutil/common.h> + +#include "config.h" +#include "talloc.h" +#include "command.h" +#include "input/input.h" +#include "stream/stream.h" +#include "demux/demux.h" +#include "demux/stheader.h" +#include "resolve.h" +#include "playlist.h" +#include "playlist_parser.h" +#include "sub/sub.h" +#include "sub/dec_sub.h" +#include "core/m_option.h" +#include "m_property.h" +#include "m_config.h" +#include "video/filter/vf.h" +#include "video/decode/vd.h" +#include "mp_osd.h" +#include "video/out/vo.h" +#include "video/csputils.h" +#include "playlist.h" +#include "audio/mixer.h" +#include "audio/out/ao.h" +#include "core/mp_common.h" +#include "audio/filter/af.h" +#include "video/decode/dec_video.h" +#include "audio/decode/dec_audio.h" +#include "core/path.h" +#include "stream/tv.h" +#include "stream/stream_radio.h" +#include "stream/pvr.h" +#ifdef CONFIG_DVBIN +#include "stream/dvbin.h" +#endif +#ifdef CONFIG_DVDREAD +#include "stream/stream_dvd.h" +#endif +#include "screenshot.h" + +#include "core/mp_core.h" + +static void change_video_filters(MPContext *mpctx, const char *cmd, + const char *arg); +static int set_filters(struct MPContext *mpctx, enum stream_type mediatype, + struct m_obj_settings *new_chain); + +static char *format_bitrate(int rate) +{ + return talloc_asprintf(NULL, "%d kbps", rate * 8 / 1000); +} + +static char *format_delay(double time) +{ + return talloc_asprintf(NULL, "%d ms", ROUND(time * 1000)); +} + +// Get current mouse position in OSD coordinate space. +void mp_get_osd_mouse_pos(struct MPContext *mpctx, float *x, float *y) +{ + int wx, wy; + mp_input_get_mouse_pos(mpctx->input, &wx, &wy); + float p[2] = {wx, wy}; + // Raw window coordinates (VO mouse events) to OSD resolution. + if (mpctx->video_out) + vo_control(mpctx->video_out, VOCTRL_WINDOW_TO_OSD_COORDS, p); + *x = p[0]; + *y = p[1]; +} + +// Property-option bridge. +static int mp_property_generic_option(struct m_option *prop, int action, + void *arg, MPContext *mpctx) +{ + char *optname = prop->priv; + struct m_config_option *opt = m_config_get_co(mpctx->mconfig, + bstr0(optname)); + void *valptr = opt->data; + + switch (action) { + case M_PROPERTY_GET_TYPE: + *(struct m_option *)arg = *(opt->opt); + return M_PROPERTY_OK; + case M_PROPERTY_GET: + m_option_copy(opt->opt, arg, valptr); + return M_PROPERTY_OK; + case M_PROPERTY_SET: + m_option_copy(opt->opt, valptr, arg); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Playback speed (RW) +static int mp_property_playback_speed(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + double orig_speed = opts->playback_speed; + switch (action) { + case M_PROPERTY_SET: { + opts->playback_speed = *(double *) arg; + // Adjust time until next frame flip for nosound mode + mpctx->time_frame *= orig_speed / opts->playback_speed; + if (mpctx->sh_audio) + reinit_audio_chain(mpctx); + return M_PROPERTY_OK; + } + case M_PROPERTY_PRINT: + *(char **)arg = talloc_asprintf(NULL, "x %6.2f", orig_speed); + return M_PROPERTY_OK; + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// filename with path (RO) +static int mp_property_path(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->filename) + return M_PROPERTY_UNAVAILABLE; + return m_property_strdup_ro(prop, action, arg, mpctx->filename); +} + +static int mp_property_filename(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->filename) + return M_PROPERTY_UNAVAILABLE; + char *f = (char *)mp_basename(mpctx->filename); + return m_property_strdup_ro(prop, action, arg, (*f) ? f : mpctx->filename); +} + +static int mp_property_media_title(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + char *name = NULL; + if (mpctx->resolve_result) + name = mpctx->resolve_result->title; + if (name && name[0]) + return m_property_strdup_ro(prop, action, arg, name); + if (mpctx->master_demuxer) { + name = demux_info_get(mpctx->master_demuxer, "title"); + if (name && name[0]) + return m_property_strdup_ro(prop, action, arg, name); + } + return mp_property_filename(prop, action, arg, mpctx); +} + +static int mp_property_stream_path(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct stream *stream = mpctx->stream; + if (!stream || !stream->url) + return M_PROPERTY_UNAVAILABLE; + return m_property_strdup_ro(prop, action, arg, stream->url); +} + +static int mp_property_stream_capture(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->stream) + return M_PROPERTY_UNAVAILABLE; + + if (action == M_PROPERTY_SET) { + char *filename = *(char **)arg; + stream_set_capture_file(mpctx->stream, filename); + // fall through to mp_property_generic_option + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// Demuxer name (RO) +static int mp_property_demuxer(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->master_demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + return m_property_strdup_ro(prop, action, arg, demuxer->desc->name); +} + +/// Position in the stream (RW) +static int mp_property_stream_pos(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct stream *stream = mpctx->stream; + if (!stream) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_GET: + *(int64_t *) arg = stream_tell(stream); + return M_PROPERTY_OK; + case M_PROPERTY_SET: + stream_seek(stream, *(int64_t *) arg); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Stream start offset (RO) +static int mp_property_stream_start(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + struct stream *stream = mpctx->stream; + if (!stream) + return M_PROPERTY_UNAVAILABLE; + return m_property_int64_ro(prop, action, arg, stream->start_pos); +} + +/// Stream end offset (RO) +static int mp_property_stream_end(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct stream *stream = mpctx->stream; + if (!stream) + return M_PROPERTY_UNAVAILABLE; + return m_property_int64_ro(prop, action, arg, stream->end_pos); +} + +/// Stream length (RO) +static int mp_property_stream_length(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + struct stream *stream = mpctx->stream; + if (!stream) + return M_PROPERTY_UNAVAILABLE; + return m_property_int64_ro(prop, action, arg, + stream->end_pos - stream->start_pos); +} + +// Does some magic to handle "<name>/full" as time formatted with milliseconds. +// Assumes prop is the type of the actual property. +static int property_time(m_option_t *prop, int action, void *arg, double time) +{ + switch (action) { + case M_PROPERTY_GET: + *(double *)arg = time; + return M_PROPERTY_OK; + case M_PROPERTY_KEY_ACTION: { + struct m_property_action_arg *ka = arg; + + if (strcmp(ka->key, "full") != 0) + return M_PROPERTY_UNKNOWN; + + switch (ka->action) { + case M_PROPERTY_GET: + *(double *)ka->arg = time; + return M_PROPERTY_OK; + case M_PROPERTY_PRINT: + *(char **)ka->arg = mp_format_time(time, true); + return M_PROPERTY_OK; + case M_PROPERTY_GET_TYPE: + *(struct m_option *)ka->arg = *prop; + return M_PROPERTY_OK; + } + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Current stream position in seconds (RO) +static int mp_property_stream_time_pos(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + double pts = demuxer->stream_pts; + if (pts == MP_NOPTS_VALUE) + return M_PROPERTY_UNAVAILABLE; + + return property_time(prop, action, arg, pts); +} + + +/// Media length in seconds (RO) +static int mp_property_length(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + double len; + + if (!(int) (len = get_time_length(mpctx))) + return M_PROPERTY_UNAVAILABLE; + + return property_time(prop, action, arg, len); +} + +static int mp_property_avsync(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_audio || !mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + return m_property_double_ro(prop, action, arg, mpctx->last_av_difference); +} + +/// Current position in percent (RW) +static int mp_property_percent_pos(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->num_sources) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_SET: ; + double pos = *(double *)arg; + queue_seek(mpctx, MPSEEK_FACTOR, pos / 100.0, 0); + return M_PROPERTY_OK; + case M_PROPERTY_GET: + *(double *)arg = get_current_pos_ratio(mpctx, false) * 100.0; + return M_PROPERTY_OK; + case M_PROPERTY_PRINT: + *(char **)arg = talloc_asprintf(NULL, "%d", get_percent_pos(mpctx)); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Current position in seconds (RW) +static int mp_property_time_pos(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->num_sources) + return M_PROPERTY_UNAVAILABLE; + + if (action == M_PROPERTY_SET) { + queue_seek(mpctx, MPSEEK_ABSOLUTE, *(double *)arg, 0); + return M_PROPERTY_OK; + } + return property_time(prop, action, arg, get_current_time(mpctx)); +} + +static int mp_property_remaining(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + double len = get_time_length(mpctx); + double pos = get_current_time(mpctx); + double start = get_start_time(mpctx); + + if (!(int)len) + return M_PROPERTY_UNAVAILABLE; + + return property_time(prop, action, arg, len - (pos - start)); +} + +/// Current chapter (RW) +static int mp_property_chapter(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + int chapter = get_current_chapter(mpctx); + if (chapter < -1) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_GET: + *(int *) arg = chapter; + return M_PROPERTY_OK; + case M_PROPERTY_PRINT: { + char *chapter_name = chapter_display_name(mpctx, chapter); + if (!chapter_name) + return M_PROPERTY_UNAVAILABLE; + *(char **) arg = chapter_name; + return M_PROPERTY_OK; + } + case M_PROPERTY_SET: ; + int step_all = *(int *)arg - chapter; + chapter += step_all; + if (chapter >= get_chapter_count(mpctx) && step_all > 0) { + mpctx->stop_play = PT_NEXT_ENTRY; + } else { + mp_seek_chapter(mpctx, chapter); + } + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static int mp_property_list_chapters(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (action == M_PROPERTY_GET) { + int count = get_chapter_count(mpctx); + int cur = mpctx->num_sources ? get_current_chapter(mpctx) : -1; + char *res = NULL; + int n; + + if (count < 1) { + res = talloc_asprintf_append(res, "No chapters."); + } + + for (n = 0; n < count; n++) { + char *name = chapter_display_name(mpctx, n); + double t = chapter_start_time(mpctx, n); + char* time = mp_format_time(t, false); + res = talloc_asprintf_append(res, "%s", time); + talloc_free(time); + char *m1 = "> ", *m2 = " <"; + if (n != cur) + m1 = m2 = ""; + res = talloc_asprintf_append(res, " %s%s%s\n", m1, name, m2); + talloc_free(name); + } + + *(char **)arg = res; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static int mp_property_edition(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + struct demuxer *demuxer = mpctx->master_demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + if (demuxer->num_editions <= 0) + return M_PROPERTY_UNAVAILABLE; + + int edition = demuxer->edition; + + switch (action) { + case M_PROPERTY_GET: + *(int *)arg = edition; + return M_PROPERTY_OK; + case M_PROPERTY_SET: { + edition = *(int *)arg; + if (edition != demuxer->edition) { + opts->edition_id = edition; + mpctx->stop_play = PT_RESTART; + } + return M_PROPERTY_OK; + } + case M_PROPERTY_GET_TYPE: { + struct m_option opt = { + .name = prop->name, + .type = CONF_TYPE_INT, + .flags = CONF_RANGE, + .min = 0, + .max = demuxer->num_editions - 1, + }; + *(struct m_option *)arg = opt; + return M_PROPERTY_OK; + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static struct mp_resolve_src *find_source(struct mp_resolve_result *res, + char *url) +{ + if (res->num_srcs == 0) + return NULL; + + int src = 0; + for (int n = 0; n < res->num_srcs; n++) { + if (strcmp(res->srcs[n]->url, res->url) == 0) { + src = n; + break; + } + } + return res->srcs[src]; +} + +static int mp_property_quvi_format(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct mp_resolve_result *res = mpctx->resolve_result; + if (!res || !res->num_srcs) + return M_PROPERTY_UNAVAILABLE; + + struct mp_resolve_src *cur = find_source(res, res->url); + if (!cur) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_GET: + *(char **)arg = talloc_strdup(NULL, cur->encid); + return M_PROPERTY_OK; + case M_PROPERTY_SET: { + mpctx->stop_play = PT_RESTART; + break; + } + case M_PROPERTY_SWITCH: { + struct m_property_switch_arg *sarg = arg; + int pos = 0; + for (int n = 0; n < res->num_srcs; n++) { + if (res->srcs[n] == cur) { + pos = n; + break; + } + } + pos += sarg->inc; + if (pos < 0 || pos >= res->num_srcs) { + if (sarg->wrap) { + pos = (res->num_srcs + pos) % res->num_srcs; + } else { + pos = av_clip(pos, 0, res->num_srcs); + } + } + char *fmt = res->srcs[pos]->encid; + return mp_property_quvi_format(prop, M_PROPERTY_SET, &fmt, mpctx); + } + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// Number of titles in file +static int mp_property_titles(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->master_demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + int num_titles = 0; + stream_control(demuxer->stream, STREAM_CTRL_GET_NUM_TITLES, &num_titles); + return m_property_int_ro(prop, action, arg, num_titles); +} + +/// Number of chapters in file +static int mp_property_chapters(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->num_sources) + return M_PROPERTY_UNAVAILABLE; + int count = get_chapter_count(mpctx); + return m_property_int_ro(prop, action, arg, count); +} + +static int mp_property_editions(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->master_demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + if (demuxer->num_editions <= 0) + return M_PROPERTY_UNAVAILABLE; + return m_property_int_ro(prop, action, arg, demuxer->num_editions); +} + +/// Current dvd angle (RW) +static int mp_property_angle(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->master_demuxer; + int angle = -1; + int angles; + + if (demuxer) + angle = demuxer_get_current_angle(demuxer); + if (angle < 0) + return M_PROPERTY_UNAVAILABLE; + angles = demuxer_angles_count(demuxer); + if (angles <= 1) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_GET: + *(int *) arg = angle; + return M_PROPERTY_OK; + case M_PROPERTY_PRINT: { + *(char **) arg = talloc_asprintf(NULL, "%d/%d", angle, angles); + return M_PROPERTY_OK; + } + case M_PROPERTY_SET: + angle = demuxer_set_angle(demuxer, *(int *)arg); + if (angle >= 0) { + if (mpctx->sh_video) + resync_video_stream(mpctx->sh_video); + + if (mpctx->sh_audio) + resync_audio_stream(mpctx->sh_audio); + } + return M_PROPERTY_OK; + case M_PROPERTY_GET_TYPE: { + struct m_option opt = { + .name = prop->name, + .type = CONF_TYPE_INT, + .flags = CONF_RANGE, + .min = 1, + .max = angles, + }; + *(struct m_option *)arg = opt; + return M_PROPERTY_OK; + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Demuxer meta data +static int mp_property_metadata(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->master_demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + + static const m_option_t key_type = + { + "metadata", NULL, CONF_TYPE_STRING, 0, 0, 0, NULL + }; + + switch (action) { + case M_PROPERTY_GET: { + char **slist = NULL; + m_option_copy(prop, &slist, &demuxer->info); + *(char ***)arg = slist; + return M_PROPERTY_OK; + } + case M_PROPERTY_PRINT: { + char **list = demuxer->info; + char *res = NULL; + for (int n = 0; list && list[n]; n += 2) { + res = talloc_asprintf_append_buffer(res, "%s: %s\n", + list[n], list[n + 1]); + } + *(char **)arg = res; + return res ? M_PROPERTY_OK : M_PROPERTY_UNAVAILABLE; + } + case M_PROPERTY_KEY_ACTION: { + struct m_property_action_arg *ka = arg; + char *meta = demux_info_get(demuxer, ka->key); + if (!meta) + return M_PROPERTY_UNKNOWN; + switch (ka->action) { + case M_PROPERTY_GET: + *(char **)ka->arg = talloc_strdup(NULL, meta); + return M_PROPERTY_OK; + case M_PROPERTY_GET_TYPE: + *(struct m_option *)ka->arg = key_type; + return M_PROPERTY_OK; + } + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static int mp_property_pause(m_option_t *prop, int action, void *arg, + void *ctx) +{ + MPContext *mpctx = ctx; + + if (action == M_PROPERTY_SET) { + if (*(int *)arg) { + pause_player(mpctx); + } else { + unpause_player(mpctx); + } + return M_PROPERTY_OK; + } + return mp_property_generic_option(prop, action, arg, ctx); +} + +static int mp_property_cache(m_option_t *prop, int action, void *arg, + void *ctx) +{ + MPContext *mpctx = ctx; + int cache = mp_get_cache_percent(mpctx); + if (cache < 0) + return M_PROPERTY_UNAVAILABLE; + return m_property_int_ro(prop, action, arg, cache); +} + +static int mp_property_clock(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + char outstr[6]; + time_t t = time(NULL); + struct tm *tmp = localtime(&t); + + if ((tmp != NULL) && (strftime(outstr, sizeof(outstr), "%H:%M", tmp) == 5)) + return m_property_strdup_ro(prop, action, arg, outstr); + return M_PROPERTY_UNAVAILABLE; +} + +/// Volume (RW) +static int mp_property_volume(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + + if (!mpctx->sh_audio) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_GET: + mixer_getbothvolume(&mpctx->mixer, arg); + return M_PROPERTY_OK; + case M_PROPERTY_SET: + mixer_setvolume(&mpctx->mixer, *(float *) arg, *(float *) arg); + return M_PROPERTY_OK; + case M_PROPERTY_SWITCH: { + struct m_property_switch_arg *sarg = arg; + if (sarg->inc <= 0) + mixer_decvolume(&mpctx->mixer); + else + mixer_incvolume(&mpctx->mixer); + return M_PROPERTY_OK; + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Mute (RW) +static int mp_property_mute(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + + if (!mpctx->sh_audio) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_SET: + mixer_setmute(&mpctx->mixer, *(int *) arg); + return M_PROPERTY_OK; + case M_PROPERTY_GET: + *(int *)arg = mixer_getmute(&mpctx->mixer); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Audio delay (RW) +static int mp_property_audio_delay(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!(mpctx->sh_audio && mpctx->sh_video)) + return M_PROPERTY_UNAVAILABLE; + float delay = mpctx->opts->audio_delay; + switch (action) { + case M_PROPERTY_PRINT: + *(char **)arg = format_delay(delay); + return M_PROPERTY_OK; + case M_PROPERTY_SET: + mpctx->audio_delay = mpctx->opts->audio_delay = *(float *)arg; + mpctx->delay -= mpctx->audio_delay - delay; + return M_PROPERTY_OK; + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// Audio codec tag (RO) +static int mp_property_audio_format(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + const char *c = mpctx->sh_audio ? mpctx->sh_audio->gsh->codec : NULL; + return m_property_strdup_ro(prop, action, arg, c); +} + +/// Audio codec name (RO) +static int mp_property_audio_codec(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + const char *c = mpctx->sh_audio ? mpctx->sh_audio->gsh->decoder_desc : NULL; + return m_property_strdup_ro(prop, action, arg, c); +} + +/// Audio bitrate (RO) +static int mp_property_audio_bitrate(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->sh_audio) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_PRINT: + *(char **)arg = format_bitrate(mpctx->sh_audio->i_bps); + return M_PROPERTY_OK; + case M_PROPERTY_GET: + *(int *)arg = mpctx->sh_audio->i_bps; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Samplerate (RO) +static int mp_property_samplerate(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_audio) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_PRINT: + *(char **)arg = talloc_asprintf(NULL, "%d kHz", + mpctx->sh_audio->samplerate / 1000); + return M_PROPERTY_OK; + case M_PROPERTY_GET: + *(int *)arg = mpctx->sh_audio->samplerate; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Number of channels (RO) +static int mp_property_channels(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_audio) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_PRINT: + *(char **) arg = mp_chmap_to_str(&mpctx->sh_audio->channels); + return M_PROPERTY_OK; + case M_PROPERTY_GET: + *(int *)arg = mpctx->sh_audio->channels.num; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Balance (RW) +static int mp_property_balance(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + float bal; + + switch (action) { + case M_PROPERTY_GET: + mixer_getbalance(&mpctx->mixer, arg); + return M_PROPERTY_OK; + case M_PROPERTY_PRINT: { + char **str = arg; + mixer_getbalance(&mpctx->mixer, &bal); + if (bal == 0.f) + *str = talloc_strdup(NULL, "center"); + else if (bal == -1.f) + *str = talloc_strdup(NULL, "left only"); + else if (bal == 1.f) + *str = talloc_strdup(NULL, "right only"); + else { + unsigned right = (bal + 1.f) / 2.f * 100.f; + *str = talloc_asprintf(NULL, "left %d%%, right %d%%", + 100 - right, right); + } + return M_PROPERTY_OK; + } + case M_PROPERTY_SET: + mixer_setbalance(&mpctx->mixer, *(float *)arg); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static struct track* track_next(struct MPContext *mpctx, enum stream_type type, + int direction, struct track *track) +{ + assert(direction == -1 || direction == +1); + struct track *prev = NULL, *next = NULL; + bool seen = track == NULL; + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *cur = mpctx->tracks[n]; + if (cur->type == type) { + if (cur == track) { + seen = true; + } else { + if (seen && !next) { + next = cur; + } + if (!seen || !track) { + prev = cur; + } + } + } + } + return direction > 0 ? next : prev; +} + +static int property_switch_track(m_option_t *prop, int action, void *arg, + MPContext *mpctx, enum stream_type type) +{ + if (!mpctx->num_sources) + return M_PROPERTY_UNAVAILABLE; + struct track *track = mpctx->current_track[type]; + + switch (action) { + case M_PROPERTY_GET: + *(int *) arg = track ? track->user_tid : -2; + return M_PROPERTY_OK; + case M_PROPERTY_PRINT: + if (!track) + *(char **) arg = talloc_strdup(NULL, "no"); + else { + char *lang = track->lang; + if (!lang) + lang = mp_gtext("unknown"); + + if (track->title) + *(char **)arg = talloc_asprintf(NULL, "(%d) %s (\"%s\")", + track->user_tid, lang, track->title); + else + *(char **)arg = talloc_asprintf(NULL, "(%d) %s", + track->user_tid, lang); + } + return M_PROPERTY_OK; + + case M_PROPERTY_SWITCH: { + struct m_property_switch_arg *sarg = arg; + mp_switch_track(mpctx, type, + track_next(mpctx, type, sarg->inc >= 0 ? +1 : -1, track)); + return M_PROPERTY_OK; + } + case M_PROPERTY_SET: + mp_switch_track(mpctx, type, mp_track_by_tid(mpctx, type, *(int *)arg)); + return M_PROPERTY_OK; + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +static const char *track_type_name(enum stream_type t) +{ + switch (t) { + case STREAM_VIDEO: return "Video"; + case STREAM_AUDIO: return "Audio"; + case STREAM_SUB: return "Sub"; + } + return NULL; +} + +static int property_list_tracks(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (action == M_PROPERTY_GET) { + char *res = NULL; + + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *track = mpctx->tracks[n]; + if (track->type != type) + continue; + + bool selected = mpctx->current_track[track->type] == track; + res = talloc_asprintf_append(res, "%s: ", + track_type_name(track->type)); + if (selected) + res = talloc_asprintf_append(res, "> "); + res = talloc_asprintf_append(res, "(%d) ", track->user_tid); + if (track->title) + res = talloc_asprintf_append(res, "'%s' ", track->title); + if (track->lang) + res = talloc_asprintf_append(res, "(%s) ", track->lang); + if (track->is_external) + res = talloc_asprintf_append(res, "(external) "); + if (selected) + res = talloc_asprintf_append(res, "<"); + res = talloc_asprintf_append(res, "\n"); + } + + res = talloc_asprintf_append(res, "\n"); + } + + struct demuxer *demuxer = mpctx->master_demuxer; + if (demuxer && demuxer->num_editions > 1) + res = talloc_asprintf_append(res, "\nEdition: %d of %d\n", + demuxer->edition + 1, + demuxer->num_editions); + + *(char **)arg = res; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +/// Selected audio id (RW) +static int mp_property_audio(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_switch_track(prop, action, arg, mpctx, STREAM_AUDIO); +} + +/// Selected video id (RW) +static int mp_property_video(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_switch_track(prop, action, arg, mpctx, STREAM_VIDEO); +} + +static struct track *find_track_by_demuxer_id(MPContext *mpctx, + enum stream_type type, + int demuxer_id) +{ + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *track = mpctx->tracks[n]; + if (track->type == type && track->demuxer_id == demuxer_id) + return track; + } + return NULL; +} + +static int mp_property_program(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + demux_program_t prog; + + struct demuxer *demuxer = mpctx->master_demuxer; + if (!demuxer) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_SWITCH: + case M_PROPERTY_SET: + if (action == M_PROPERTY_SET && arg) + prog.progid = *((int *) arg); + else + prog.progid = -1; + if (demux_control(demuxer, DEMUXER_CTRL_IDENTIFY_PROGRAM, &prog) == + DEMUXER_CTRL_NOTIMPL) + return M_PROPERTY_ERROR; + + if (prog.aid < 0 && prog.vid < 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, + "Selected program contains no audio or video streams!\n"); + return M_PROPERTY_ERROR; + } + mp_switch_track(mpctx, STREAM_VIDEO, + find_track_by_demuxer_id(mpctx, STREAM_VIDEO, prog.vid)); + mp_switch_track(mpctx, STREAM_AUDIO, + find_track_by_demuxer_id(mpctx, STREAM_AUDIO, prog.aid)); + mp_switch_track(mpctx, STREAM_SUB, + find_track_by_demuxer_id(mpctx, STREAM_VIDEO, prog.sid)); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + + +/// Fullscreen state (RW) +static int mp_property_fullscreen(m_option_t *prop, + int action, + void *arg, + MPContext *mpctx) +{ + if (!mpctx->video_out) + return M_PROPERTY_UNAVAILABLE; + struct mp_vo_opts *opts = mpctx->video_out->opts; + + if (action == M_PROPERTY_SET) { + int val = *(int *)arg; + opts->fullscreen = val; + if (mpctx->video_out->config_ok) + vo_control(mpctx->video_out, VOCTRL_FULLSCREEN, 0); + return opts->fullscreen == val ? M_PROPERTY_OK : M_PROPERTY_ERROR; + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +#define VF_DEINTERLACE_LABEL "deinterlace" + +#ifdef CONFIG_VF_LAVFI +#define VF_DEINTERLACE "@" VF_DEINTERLACE_LABEL ":lavfi=yadif" +#else +#define VF_DEINTERLACE "@" VF_DEINTERLACE_LABEL ":yadif" +#endif + +static int get_deinterlacing(struct MPContext *mpctx) +{ + vf_instance_t *vf = mpctx->sh_video->vfilter; + int enabled = 0; + if (vf->control(vf, VFCTRL_GET_DEINTERLACE, &enabled) != CONTROL_OK) + enabled = -1; + if (enabled < 0) { + // vf_lavfi doesn't support VFCTRL_GET_DEINTERLACE + if (vf_find_by_label(vf, VF_DEINTERLACE_LABEL)) + enabled = 1; + } + return enabled; +} + +static void set_deinterlacing(struct MPContext *mpctx, bool enable) +{ + vf_instance_t *vf = mpctx->sh_video->vfilter; + if (vf_find_by_label(vf, VF_DEINTERLACE_LABEL)) { + if (!enable) + change_video_filters(mpctx, "del", VF_DEINTERLACE); + } else { + int arg = enable; + if (vf->control(vf, VFCTRL_SET_DEINTERLACE, &arg) != CONTROL_OK) + change_video_filters(mpctx, "add", VF_DEINTERLACE); + } +} + +static int mp_property_deinterlace(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->sh_video || !mpctx->sh_video->vfilter) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_GET: + *(int *)arg = get_deinterlacing(mpctx) > 0; + return M_PROPERTY_OK; + case M_PROPERTY_SET: + set_deinterlacing(mpctx, *(int *)arg); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +// Generic option + requires hard refresh to make changes take effect. +static int video_refresh_property_helper(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + int r = mp_property_generic_option(prop, action, arg, mpctx); + if (action == M_PROPERTY_SET) { + if (mpctx->sh_video) { + reinit_video_filters(mpctx); + mp_force_video_refresh(mpctx); + } + } + return r; +} + +static int mp_property_colormatrix(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (action != M_PROPERTY_PRINT) + return video_refresh_property_helper(prop, action, arg, mpctx); + + struct MPOpts *opts = mpctx->opts; + + struct mp_csp_details vo_csp = {0}; + if (mpctx->sh_video && mpctx->sh_video->vfilter) + vf_control(mpctx->sh_video->vfilter, VFCTRL_GET_YUV_COLORSPACE, &vo_csp); + + struct mp_image_params vd_csp = {0}; + if (mpctx->sh_video) + vd_control(mpctx->sh_video, VDCTRL_GET_PARAMS, &vd_csp); + + char *res = talloc_asprintf(NULL, "%s", + mp_csp_names[opts->requested_colorspace]); + if (!vo_csp.format) { + res = talloc_asprintf_append(res, " (VO: unknown)"); + } else if (vo_csp.format != opts->requested_colorspace) { + res = talloc_asprintf_append(res, " (VO: %s)", + mp_csp_names[vo_csp.format]); + } + if (!vd_csp.colorspace) { + res = talloc_asprintf_append(res, " (VD: unknown)"); + } else if (!vo_csp.format || vd_csp.colorspace != vo_csp.format) { + res = talloc_asprintf_append(res, " (VD: %s)", + mp_csp_names[vd_csp.colorspace]); + } + *(char **)arg = res; + return M_PROPERTY_OK; +} + +static int mp_property_colormatrix_input_range(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (action != M_PROPERTY_PRINT) + return video_refresh_property_helper(prop, action, arg, mpctx); + + struct MPOpts *opts = mpctx->opts; + + struct mp_csp_details vo_csp = {0}; + if (mpctx->sh_video && mpctx->sh_video->vfilter) + vf_control(mpctx->sh_video->vfilter, VFCTRL_GET_YUV_COLORSPACE, &vo_csp ); + + struct mp_image_params vd_csp = {0}; + if (mpctx->sh_video) + vd_control(mpctx->sh_video, VDCTRL_GET_PARAMS, &vd_csp); + + char *res = talloc_asprintf(NULL, "%s", + mp_csp_levels_names[opts->requested_input_range]); + if (!vo_csp.levels_in) { + res = talloc_asprintf_append(res, " (VO: unknown)"); + } else if (vo_csp.levels_in != opts->requested_input_range) { + res = talloc_asprintf_append(res, " (VO: %s)", + mp_csp_levels_names[vo_csp.levels_in]); + } + if (!vd_csp.colorlevels) { + res = talloc_asprintf_append(res, " (VD: unknown)"); + } else if (!vo_csp.levels_in || vd_csp.colorlevels != vo_csp.levels_in) { + res = talloc_asprintf_append(res, " (VD: %s)", + mp_csp_levels_names[vd_csp.colorlevels]); + } + *(char **)arg = res; + return M_PROPERTY_OK; +} + +static int mp_property_colormatrix_output_range(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (action != M_PROPERTY_PRINT) { + int r = mp_property_generic_option(prop, action, arg, mpctx); + if (action == M_PROPERTY_SET) { + if (mpctx->sh_video) + set_video_output_levels(mpctx->sh_video); + } + return r; + } + + struct MPOpts *opts = mpctx->opts; + + int req = opts->requested_output_range; + struct mp_csp_details actual = {0}; + if (mpctx->sh_video && mpctx->sh_video->vfilter) + vf_control(mpctx->sh_video->vfilter, VFCTRL_GET_YUV_COLORSPACE, &actual); + + char *res = talloc_asprintf(NULL, "%s", mp_csp_levels_names[req]); + if (!actual.levels_out) { + res = talloc_asprintf_append(res, " (Actual: unknown)"); + } else if (actual.levels_out != req) { + res = talloc_asprintf_append(res, " (Actual: %s)", + mp_csp_levels_names[actual.levels_out]); + } + *(char **)arg = res; + return M_PROPERTY_OK; +} + +/// Panscan (RW) +static int mp_property_panscan(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + + if (!mpctx->video_out + || vo_control(mpctx->video_out, VOCTRL_GET_PANSCAN, NULL) != VO_TRUE) + return M_PROPERTY_UNAVAILABLE; + + int r = mp_property_generic_option(prop, action, arg, mpctx); + if (action == M_PROPERTY_SET) + vo_control(mpctx->video_out, VOCTRL_SET_PANSCAN, NULL); + return r; +} + +/// Helper to set vo flags. +/** \ingroup PropertyImplHelper + */ +static int mp_property_vo_flag(m_option_t *prop, int action, void *arg, + int vo_ctrl, int *vo_var, MPContext *mpctx) +{ + + if (!mpctx->video_out) + return M_PROPERTY_UNAVAILABLE; + + if (action == M_PROPERTY_SET) { + if (*vo_var == !!*(int *) arg) + return M_PROPERTY_OK; + if (mpctx->video_out->config_ok) + vo_control(mpctx->video_out, vo_ctrl, 0); + return M_PROPERTY_OK; + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// Window always on top (RW) +static int mp_property_ontop(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return mp_property_vo_flag(prop, action, arg, VOCTRL_ONTOP, + &mpctx->opts->vo.ontop, mpctx); +} + +/// Show window borders (RW) +static int mp_property_border(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return mp_property_vo_flag(prop, action, arg, VOCTRL_BORDER, + &mpctx->opts->vo.border, mpctx); +} + +static int mp_property_framedrop(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// Color settings, try to use vf/vo then fall back on TV. (RW) +static int mp_property_gamma(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + int *gamma = (int *)((char *)mpctx->opts + prop->offset); + int r, val; + + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + + if (gamma[0] == 1000) { + gamma[0] = 0; + get_video_colors(mpctx->sh_video, prop->name, gamma); + } + + switch (action) { + case M_PROPERTY_SET: + *gamma = *(int *) arg; + r = set_video_colors(mpctx->sh_video, prop->name, *gamma); + if (r <= 0) + break; + return r; + case M_PROPERTY_GET: + if (get_video_colors(mpctx->sh_video, prop->name, &val) > 0) { + *(int *)arg = val; + return M_PROPERTY_OK; + } + break; + default: + return mp_property_generic_option(prop, action, arg, mpctx); + } + +#ifdef CONFIG_TV + if (mpctx->sh_video->gsh->demuxer->type == DEMUXER_TYPE_TV) { + int l = strlen(prop->name); + char tv_prop[3 + l + 1]; + sprintf(tv_prop, "tv-%s", prop->name); + return mp_property_do(tv_prop, action, arg, mpctx); + } +#endif + + return M_PROPERTY_UNAVAILABLE; +} + +/// Video codec tag (RO) +static int mp_property_video_format(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + const char *c = mpctx->sh_video ? mpctx->sh_video->gsh->codec : NULL; + return m_property_strdup_ro(prop, action, arg, c); +} + +/// Video codec name (RO) +static int mp_property_video_codec(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + const char *c = mpctx->sh_video ? mpctx->sh_video->gsh->decoder_desc : NULL; + return m_property_strdup_ro(prop, action, arg, c); +} + + +/// Video bitrate (RO) +static int mp_property_video_bitrate(m_option_t *prop, int action, + void *arg, MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + if (action == M_PROPERTY_PRINT) { + *(char **)arg = format_bitrate(mpctx->sh_video->i_bps); + return M_PROPERTY_OK; + } + return m_property_int_ro(prop, action, arg, mpctx->sh_video->i_bps); +} + +/// Video display width (RO) +static int mp_property_width(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + return m_property_int_ro(prop, action, arg, mpctx->sh_video->disp_w); +} + +/// Video display height (RO) +static int mp_property_height(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + return m_property_int_ro(prop, action, arg, mpctx->sh_video->disp_h); +} + +static int property_vo_wh(m_option_t *prop, int action, void *arg, + MPContext *mpctx, bool get_w) +{ + struct vo *vo = mpctx->video_out; + if (!mpctx->sh_video && !vo || !vo->hasframe) + return M_PROPERTY_UNAVAILABLE; + return m_property_int_ro(prop, action, arg, + get_w ? vo->aspdat.prew : vo->aspdat.preh); +} + +static int mp_property_dwidth(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_vo_wh(prop, action, arg, mpctx, true); +} + +static int mp_property_dheight(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_vo_wh(prop, action, arg, mpctx, false); +} + +/// Video fps (RO) +static int mp_property_fps(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + return m_property_float_ro(prop, action, arg, mpctx->sh_video->fps); +} + +/// Video aspect (RO) +static int mp_property_aspect(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_SET: { + float f = *(float *)arg; + if (f < 0.1) + f = (float)mpctx->sh_video->disp_w / mpctx->sh_video->disp_h; + mpctx->opts->movie_aspect = f; + video_reinit_vo(mpctx->sh_video); + return M_PROPERTY_OK; + } + case M_PROPERTY_GET: + *(float *)arg = mpctx->sh_video->aspect; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +// For OSD and subtitle related properties using the generic option bridge. +// - Fail as unavailable if no video is active +// - Trigger OSD state update when property is set +static int property_osd_helper(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + if (action == M_PROPERTY_SET) + osd_changed_all(mpctx->osd); + return mp_property_generic_option(prop, action, arg, mpctx); +} + +/// Selected subtitles (RW) +static int mp_property_sub(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_switch_track(prop, action, arg, mpctx, STREAM_SUB); +} + +/// Subtitle delay (RW) +static int mp_property_sub_delay(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + switch (action) { + case M_PROPERTY_PRINT: + *(char **)arg = format_delay(opts->sub_delay); + return M_PROPERTY_OK; + } + return property_osd_helper(prop, action, arg, mpctx); +} + +static int mp_property_sub_pos(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + if (!mpctx->sh_video) + return M_PROPERTY_UNAVAILABLE; + if (action == M_PROPERTY_PRINT) { + *(char **)arg = talloc_asprintf(NULL, "%d/100", opts->sub_pos); + return M_PROPERTY_OK; + } + return property_osd_helper(prop, action, arg, mpctx); +} + +#ifdef CONFIG_TV + +static tvi_handle_t *get_tvh(struct MPContext *mpctx) +{ + if (!(mpctx->master_demuxer && mpctx->master_demuxer->type == DEMUXER_TYPE_TV)) + return NULL; + return mpctx->master_demuxer->priv; +} + +/// TV color settings (RW) +static int mp_property_tv_color(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + tvi_handle_t *tvh = get_tvh(mpctx); + if (!tvh) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_SET: + return tv_set_color_options(tvh, prop->offset, *(int *) arg); + case M_PROPERTY_GET: + return tv_get_color_options(tvh, prop->offset, arg); + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +#endif + +static int mp_property_playlist_pos(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + struct playlist *pl = mpctx->playlist; + if (!pl->first) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_GET: { + int pos = playlist_entry_to_index(pl, pl->current); + if (pos < 0) + return M_PROPERTY_UNAVAILABLE; + *(int *)arg = pos; + return M_PROPERTY_OK; + } + case M_PROPERTY_SET: { + struct playlist_entry *e = playlist_entry_from_index(pl, *(int *)arg); + if (!e) + return M_PROPERTY_ERROR; + mp_set_playlist_entry(mpctx, e); + return M_PROPERTY_OK; + } + case M_PROPERTY_GET_TYPE: { + struct m_option opt = { + .name = prop->name, + .type = CONF_TYPE_INT, + .flags = CONF_RANGE, + .min = 0, + .max = playlist_entry_count(pl) - 1, + }; + *(struct m_option *)arg = opt; + return M_PROPERTY_OK; + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static int mp_property_playlist_count(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (action == M_PROPERTY_GET) { + *(int *)arg = playlist_entry_count(mpctx->playlist); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static int mp_property_playlist(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (action == M_PROPERTY_GET) { + char *res = talloc_strdup(NULL, ""); + + for (struct playlist_entry *e = mpctx->playlist->first; e; e = e->next) + { + if (mpctx->playlist->current == e) { + res = talloc_asprintf_append(res, "> %s <\n", e->filename); + } else { + res = talloc_asprintf_append(res, "%s\n", e->filename); + } + } + + *(char **)arg = res; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +static char *print_obj_osd_list(struct m_obj_settings *list) +{ + char *res = NULL; + for (int n = 0; list && list[n].name; n++) { + res = talloc_asprintf_append(res, "%s [", list[n].name); + for (int i = 0; list[n].attribs && list[n].attribs[i]; i += 2) { + res = talloc_asprintf_append(res, "%s%s=%s", i > 0 ? " " : "", + list[n].attribs[i], + list[n].attribs[i + 1]); + } + res = talloc_asprintf_append(res, "]\n"); + } + if (!res) + res = talloc_strdup(NULL, "(empty)"); + return res; +} + +static int property_filter(m_option_t *prop, int action, void *arg, + MPContext *mpctx, enum stream_type mt) +{ + switch (action) { + case M_PROPERTY_PRINT: { + struct m_config_option *opt = m_config_get_co(mpctx->mconfig, + bstr0(prop->name)); + *(char **)arg = print_obj_osd_list(*(struct m_obj_settings **)opt->data); + return M_PROPERTY_OK; + } + case M_PROPERTY_SET: + return set_filters(mpctx, mt, *(struct m_obj_settings **)arg) >= 0 + ? M_PROPERTY_OK : M_PROPERTY_ERROR; + } + return mp_property_generic_option(prop, action, arg, mpctx); +} + +static int mp_property_vf(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_filter(prop, action, arg, mpctx, STREAM_VIDEO); +} + +static int mp_property_af(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + return property_filter(prop, action, arg, mpctx, STREAM_AUDIO); +} + +static int mp_property_alias(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + const char *real_property = prop->priv; + int r = mp_property_do(real_property, action, arg, mpctx); + if (action == M_PROPERTY_GET_TYPE && r >= 0) { + // Fix the property name + struct m_option *type = arg; + type->name = prop->name; + } + return r; +} + +static int mp_property_options(m_option_t *prop, int action, void *arg, + MPContext *mpctx) +{ + if (action != M_PROPERTY_KEY_ACTION) + return M_PROPERTY_NOT_IMPLEMENTED; + + struct m_property_action_arg *ka = arg; + + struct m_config_option *opt = m_config_get_co(mpctx->mconfig, + bstr0(ka->key)); + if (!opt) + return M_PROPERTY_UNKNOWN; + if (!opt->data) + return M_PROPERTY_UNAVAILABLE; + + switch (ka->action) { + case M_PROPERTY_GET: + m_option_copy(opt->opt, ka->arg, opt->data); + return M_PROPERTY_OK; + case M_PROPERTY_GET_TYPE: + *(struct m_option *)ka->arg = *opt->opt; + return M_PROPERTY_OK; + } + + return M_PROPERTY_NOT_IMPLEMENTED; +} + +// Use option-to-property-bridge. (The property and option have the same names.) +#define M_OPTION_PROPERTY(name) \ + {(name), mp_property_generic_option, &m_option_type_dummy, 0, 0, 0, (name)} + +// OPTION_PROPERTY(), but with a custom property handler. The custom handler +// must let unknown operations fall back to mp_property_generic_option(). +#define M_OPTION_PROPERTY_CUSTOM(name, handler) \ + {(name), (handler), &m_option_type_dummy, 0, 0, 0, (name)} +#define M_OPTION_PROPERTY_CUSTOM_(name, handler, ...) \ + {(name), (handler), &m_option_type_dummy, 0, 0, 0, (name), __VA_ARGS__} + +// Redirect a property name to another +#define M_PROPERTY_ALIAS(name, real_property) \ + {(name), mp_property_alias, &m_option_type_dummy, 0, 0, 0, (real_property)} + +/// All properties available in MPlayer. +/** \ingroup Properties + */ +static const m_option_t mp_properties[] = { + // General + M_OPTION_PROPERTY("osd-level"), + M_OPTION_PROPERTY_CUSTOM("osd-scale", property_osd_helper), + M_OPTION_PROPERTY("loop"), + M_OPTION_PROPERTY_CUSTOM("speed", mp_property_playback_speed), + { "filename", mp_property_filename, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "path", mp_property_path, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "media-title", mp_property_media_title, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "stream-path", mp_property_stream_path, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + M_OPTION_PROPERTY_CUSTOM("stream-capture", mp_property_stream_capture), + { "demuxer", mp_property_demuxer, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "stream-pos", mp_property_stream_pos, CONF_TYPE_INT64, + M_OPT_MIN, 0, 0, NULL }, + { "stream-start", mp_property_stream_start, CONF_TYPE_INT64, + M_OPT_MIN, 0, 0, NULL }, + { "stream-end", mp_property_stream_end, CONF_TYPE_INT64, + M_OPT_MIN, 0, 0, NULL }, + { "stream-length", mp_property_stream_length, CONF_TYPE_INT64, + M_OPT_MIN, 0, 0, NULL }, + { "stream-time-pos", mp_property_stream_time_pos, CONF_TYPE_TIME, + M_OPT_MIN, 0, 0, NULL }, + { "length", mp_property_length, CONF_TYPE_TIME, + M_OPT_MIN, 0, 0, NULL }, + { "avsync", mp_property_avsync, CONF_TYPE_DOUBLE }, + { "percent-pos", mp_property_percent_pos, CONF_TYPE_DOUBLE, + M_OPT_RANGE, 0, 100, NULL }, + { "time-pos", mp_property_time_pos, CONF_TYPE_TIME, + M_OPT_MIN, 0, 0, NULL }, + { "time-remaining", mp_property_remaining, CONF_TYPE_TIME }, + { "chapter", mp_property_chapter, CONF_TYPE_INT, + M_OPT_MIN, 0, 0, NULL }, + M_OPTION_PROPERTY_CUSTOM("edition", mp_property_edition), + M_OPTION_PROPERTY_CUSTOM("quvi-format", mp_property_quvi_format), + { "titles", mp_property_titles, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "chapters", mp_property_chapters, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "editions", mp_property_editions, CONF_TYPE_INT }, + { "angle", mp_property_angle, &m_option_type_dummy }, + { "metadata", mp_property_metadata, CONF_TYPE_STRING_LIST, + 0, 0, 0, NULL }, + M_OPTION_PROPERTY_CUSTOM("pause", mp_property_pause), + { "cache", mp_property_cache, CONF_TYPE_INT }, + M_OPTION_PROPERTY("pts-association-mode"), + M_OPTION_PROPERTY("hr-seek"), + { "clock", mp_property_clock, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + + { "chapter-list", mp_property_list_chapters, CONF_TYPE_STRING }, + { "track-list", property_list_tracks, CONF_TYPE_STRING }, + + { "playlist", mp_property_playlist, CONF_TYPE_STRING }, + { "playlist-pos", mp_property_playlist_pos, CONF_TYPE_INT }, + { "playlist-count", mp_property_playlist_count, CONF_TYPE_INT }, + + // Audio + { "volume", mp_property_volume, CONF_TYPE_FLOAT, + M_OPT_RANGE, 0, 100, NULL }, + { "mute", mp_property_mute, CONF_TYPE_FLAG, + M_OPT_RANGE, 0, 1, NULL }, + M_OPTION_PROPERTY_CUSTOM("audio-delay", mp_property_audio_delay), + { "audio-format", mp_property_audio_format, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "audio-codec", mp_property_audio_codec, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "audio-bitrate", mp_property_audio_bitrate, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "samplerate", mp_property_samplerate, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "channels", mp_property_channels, CONF_TYPE_INT, + 0, 0, 0, NULL }, + M_OPTION_PROPERTY_CUSTOM("aid", mp_property_audio), + { "balance", mp_property_balance, CONF_TYPE_FLOAT, + M_OPT_RANGE, -1, 1, NULL }, + + // Video + M_OPTION_PROPERTY_CUSTOM("fullscreen", mp_property_fullscreen), + { "deinterlace", mp_property_deinterlace, CONF_TYPE_FLAG, + M_OPT_RANGE, 0, 1, NULL }, + M_OPTION_PROPERTY_CUSTOM("colormatrix", mp_property_colormatrix), + M_OPTION_PROPERTY_CUSTOM("colormatrix-input-range", + mp_property_colormatrix_input_range), + M_OPTION_PROPERTY_CUSTOM("colormatrix-output-range", + mp_property_colormatrix_output_range), + M_OPTION_PROPERTY_CUSTOM("ontop", mp_property_ontop), + M_OPTION_PROPERTY_CUSTOM("border", mp_property_border), + M_OPTION_PROPERTY_CUSTOM("framedrop", mp_property_framedrop), + M_OPTION_PROPERTY_CUSTOM_("gamma", mp_property_gamma, + .offset = offsetof(struct MPOpts, gamma_gamma)), + M_OPTION_PROPERTY_CUSTOM_("brightness", mp_property_gamma, + .offset = offsetof(struct MPOpts, gamma_brightness)), + M_OPTION_PROPERTY_CUSTOM_("contrast", mp_property_gamma, + .offset = offsetof(struct MPOpts, gamma_contrast)), + M_OPTION_PROPERTY_CUSTOM_("saturation", mp_property_gamma, + .offset = offsetof(struct MPOpts, gamma_saturation)), + M_OPTION_PROPERTY_CUSTOM_("hue", mp_property_gamma, + .offset = offsetof(struct MPOpts, gamma_hue)), + M_OPTION_PROPERTY_CUSTOM("panscan", mp_property_panscan), + { "video-format", mp_property_video_format, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "video-codec", mp_property_video_codec, CONF_TYPE_STRING, + 0, 0, 0, NULL }, + { "video-bitrate", mp_property_video_bitrate, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "width", mp_property_width, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "height", mp_property_height, CONF_TYPE_INT, + 0, 0, 0, NULL }, + { "dwidth", mp_property_dwidth, CONF_TYPE_INT }, + { "dheight", mp_property_dheight, CONF_TYPE_INT }, + { "fps", mp_property_fps, CONF_TYPE_FLOAT, + 0, 0, 0, NULL }, + { "aspect", mp_property_aspect, CONF_TYPE_FLOAT, + CONF_RANGE, 0, 10, NULL }, + M_OPTION_PROPERTY_CUSTOM("vid", mp_property_video), + { "program", mp_property_program, CONF_TYPE_INT, + CONF_RANGE, -1, 65535, NULL }, + + // Subs + M_OPTION_PROPERTY_CUSTOM("sid", mp_property_sub), + M_OPTION_PROPERTY_CUSTOM("sub-delay", mp_property_sub_delay), + M_OPTION_PROPERTY_CUSTOM("sub-pos", mp_property_sub_pos), + M_OPTION_PROPERTY_CUSTOM("sub-visibility", property_osd_helper), + M_OPTION_PROPERTY_CUSTOM("sub-forced-only", property_osd_helper), + M_OPTION_PROPERTY_CUSTOM("sub-scale", property_osd_helper), +#ifdef CONFIG_ASS + M_OPTION_PROPERTY_CUSTOM("ass-use-margins", property_osd_helper), + M_OPTION_PROPERTY_CUSTOM("ass-vsfilter-aspect-compat", property_osd_helper), + M_OPTION_PROPERTY_CUSTOM("ass-style-override", property_osd_helper), +#endif + + M_OPTION_PROPERTY_CUSTOM("vf*", mp_property_vf), + M_OPTION_PROPERTY_CUSTOM("af*", mp_property_af), + +#ifdef CONFIG_TV + { "tv-brightness", mp_property_tv_color, CONF_TYPE_INT, + M_OPT_RANGE, -100, 100, .offset = TV_COLOR_BRIGHTNESS }, + { "tv-contrast", mp_property_tv_color, CONF_TYPE_INT, + M_OPT_RANGE, -100, 100, .offset = TV_COLOR_CONTRAST }, + { "tv-saturation", mp_property_tv_color, CONF_TYPE_INT, + M_OPT_RANGE, -100, 100, .offset = TV_COLOR_SATURATION }, + { "tv-hue", mp_property_tv_color, CONF_TYPE_INT, + M_OPT_RANGE, -100, 100, .offset = TV_COLOR_HUE }, +#endif + + M_PROPERTY_ALIAS("video", "vid"), + M_PROPERTY_ALIAS("audio", "aid"), + M_PROPERTY_ALIAS("sub", "sid"), + + { "options", mp_property_options, &m_option_type_dummy }, + + {0}, +}; + +int mp_property_do(const char *name, int action, void *val, + struct MPContext *ctx) +{ + return m_property_do(mp_properties, name, action, val, ctx); +} + +char *mp_property_expand_string(struct MPContext *mpctx, char *str) +{ + return m_properties_expand_string(mp_properties, str, mpctx); +} + +void property_print_help(void) +{ + m_properties_print_help_list(mp_properties); +} + + +/* List of default ways to show a property on OSD. + * + * If osd_progbar is set, a bar showing the current position between min/max + * values of the property is shown. In this case osd_msg is only used for + * terminal output if there is no video; it'll be a label shown together with + * percentage. + */ +static struct property_osd_display { + // property name + const char *name; + // name used on OSD + const char *osd_name; + // progressbar type + int osd_progbar; + // osd msg id if it must be shared + int osd_id; + // Needs special ways to display the new value (seeks are delayed) + int seek_msg, seek_bar; + // Separator between option name and value (default: ": ") + const char *sep; +} property_osd_display[] = { + // general + { "loop", _("Loop") }, + { "chapter", .seek_msg = OSD_SEEK_INFO_CHAPTER_TEXT, + .seek_bar = OSD_SEEK_INFO_BAR }, + { "edition", .seek_msg = OSD_SEEK_INFO_EDITION }, + { "pts-association-mode", "PTS association mode" }, + { "hr-seek", "hr-seek" }, + { "speed", _("Speed") }, + { "clock", _("Clock") }, + // audio + { "volume", _("Volume"), .osd_progbar = OSD_VOLUME }, + { "mute", _("Mute") }, + { "audio-delay", _("A-V delay") }, + { "audio", _("Audio") }, + { "balance", _("Balance"), .osd_progbar = OSD_BALANCE }, + // video + { "panscan", _("Panscan"), .osd_progbar = OSD_PANSCAN }, + { "ontop", _("Stay on top") }, + { "border", _("Border") }, + { "framedrop", _("Framedrop") }, + { "deinterlace", _("Deinterlace") }, + { "colormatrix", _("YUV colormatrix") }, + { "colormatrix-input-range", _("YUV input range") }, + { "colormatrix-output-range", _("RGB output range") }, + { "gamma", _("Gamma"), .osd_progbar = OSD_BRIGHTNESS }, + { "brightness", _("Brightness"), .osd_progbar = OSD_BRIGHTNESS }, + { "contrast", _("Contrast"), .osd_progbar = OSD_CONTRAST }, + { "saturation", _("Saturation"), .osd_progbar = OSD_SATURATION }, + { "hue", _("Hue"), .osd_progbar = OSD_HUE }, + { "angle", _("Angle") }, + // subs + { "sub", _("Subtitles") }, + { "sub-pos", _("Sub position") }, + { "sub-delay", _("Sub delay"), .osd_id = OSD_MSG_SUB_DELAY }, + { "sub-visibility", _("Subtitles") }, + { "sub-forced-only", _("Forced sub only") }, + { "sub-scale", _("Sub Scale")}, + { "ass-vsfilter-aspect-compat", _("Subtitle VSFilter aspect compat")}, + { "ass-style-override", _("ASS subtitle style override")}, + { "vf*", _("Video filters"), .sep = ":\n"}, + { "af*", _("Audio filters"), .sep = ":\n"}, +#ifdef CONFIG_TV + { "tv-brightness", _("Brightness"), .osd_progbar = OSD_BRIGHTNESS }, + { "tv-hue", _("Hue"), .osd_progbar = OSD_HUE}, + { "tv-saturation", _("Saturation"), .osd_progbar = OSD_SATURATION }, + { "tv-contrast", _("Contrast"), .osd_progbar = OSD_CONTRAST }, +#endif + {0} +}; + +static void show_property_osd(MPContext *mpctx, const char *pname, + enum mp_on_osd osd_mode) +{ + struct MPOpts *opts = mpctx->opts; + struct m_option prop = {0}; + struct property_osd_display *p; + + if (mp_property_do(pname, M_PROPERTY_GET_TYPE, &prop, mpctx) <= 0) + return; + + int osd_progbar = 0; + const char *osd_name = NULL; + + // look for the command + for (p = property_osd_display; p->name; p++) { + if (!strcmp(p->name, prop.name)) { + osd_progbar = p->seek_bar ? 1 : p->osd_progbar; + osd_name = p->seek_msg ? "" : mp_gtext(p->osd_name); + break; + } + } + if (!p->name) + p = NULL; + + if (osd_mode != MP_ON_OSD_AUTO) { + osd_name = osd_name ? osd_name : prop.name; + if (!(osd_mode & MP_ON_OSD_MSG)) + osd_name = NULL; + osd_progbar = osd_progbar ? osd_progbar : ' '; + if (!(osd_mode & MP_ON_OSD_BAR)) + osd_progbar = 0; + } + + if (p && (p->seek_msg || p->seek_bar)) { + mpctx->add_osd_seek_info |= + (osd_name ? p->seek_msg : 0) | (osd_progbar ? p->seek_bar : 0); + return; + } + + if (osd_progbar && (prop.flags & CONF_RANGE) == CONF_RANGE) { + bool ok = false; + if (prop.type == CONF_TYPE_INT) { + int i; + ok = mp_property_do(prop.name, M_PROPERTY_GET, &i, mpctx) > 0; + if (ok) + set_osd_bar(mpctx, osd_progbar, osd_name, prop.min, prop.max, i); + } else if (prop.type == CONF_TYPE_FLOAT) { + float f; + ok = mp_property_do(prop.name, M_PROPERTY_GET, &f, mpctx) > 0; + if (ok) + set_osd_bar(mpctx, osd_progbar, osd_name, prop.min, prop.max, f); + } + if (ok && osd_mode == MP_ON_OSD_AUTO && opts->osd_bar_visible) + return; + } + + if (osd_name) { + char *val = NULL; + int r = mp_property_do(prop.name, M_PROPERTY_PRINT, &val, mpctx); + if (r == M_PROPERTY_UNAVAILABLE) { + set_osd_tmsg(mpctx, OSD_MSG_TEXT, 1, opts->osd_duration, + "%s: (unavailable)", osd_name); + } else if (r >= 0 && val) { + int osd_id = 0; + const char *sep = NULL; + if (p) { + int index = p - property_osd_display; + osd_id = p->osd_id ? p->osd_id : OSD_MSG_PROPERTY + index; + sep = p->sep; + } + if (!sep) + sep = ": "; + set_osd_tmsg(mpctx, osd_id, 1, opts->osd_duration, + "%s%s%s", osd_name, sep, val); + talloc_free(val); + } + } +} + +static const char *property_error_string(int error_value) +{ + switch (error_value) { + case M_PROPERTY_ERROR: + return "ERROR"; + case M_PROPERTY_UNAVAILABLE: + return "PROPERTY_UNAVAILABLE"; + case M_PROPERTY_NOT_IMPLEMENTED: + return "NOT_IMPLEMENTED"; + case M_PROPERTY_UNKNOWN: + return "PROPERTY_UNKNOWN"; + } + return "UNKNOWN"; +} + +static bool reinit_filters(MPContext *mpctx, enum stream_type mediatype) +{ + switch (mediatype) { + case STREAM_VIDEO: + return reinit_video_filters(mpctx) >= 0; + case STREAM_AUDIO: + return reinit_audio_filters(mpctx) >= 0; + } + return false; +} + +static const char *filter_opt[STREAM_TYPE_COUNT] = { + [STREAM_VIDEO] = "vf", + [STREAM_AUDIO] = "af", +}; + +static int set_filters(struct MPContext *mpctx, enum stream_type mediatype, + struct m_obj_settings *new_chain) +{ + bstr option = bstr0(filter_opt[mediatype]); + struct m_config_option *co = m_config_get_co(mpctx->mconfig, option); + if (!co) + return -1; + + struct m_obj_settings **list = co->data; + struct m_obj_settings *old_settings = *list; + *list = NULL; + m_option_copy(co->opt, list, &new_chain); + + bool success = reinit_filters(mpctx, mediatype); + + if (success) { + m_option_free(co->opt, &old_settings); + } else { + m_option_free(co->opt, list); + *list = old_settings; + reinit_filters(mpctx, mediatype); + } + + if (mediatype == STREAM_VIDEO) + mp_force_video_refresh(mpctx); + + return success ? 0 : -1; +} + +static int edit_filters(struct MPContext *mpctx, enum stream_type mediatype, + const char *cmd, const char *arg) +{ + bstr option = bstr0(filter_opt[mediatype]); + struct m_config_option *co = m_config_get_co(mpctx->mconfig, option); + if (!co) + return -1; + + // The option parser is used to modify the filter list itself. + char optname[20]; + snprintf(optname, sizeof(optname), "%.*s-%s", BSTR_P(option), cmd); + + struct m_obj_settings *new_chain = NULL; + m_option_copy(co->opt, &new_chain, co->data); + + int r = m_option_parse(co->opt, bstr0(optname), bstr0(arg), &new_chain); + if (r >= 0) + r = set_filters(mpctx, mediatype, new_chain); + + m_option_free(co->opt, &new_chain); + + return r >= 0 ? 0 : -1; +} + +static int edit_filters_osd(struct MPContext *mpctx, enum stream_type mediatype, + const char *cmd, const char *arg, bool on_osd) +{ + int r = edit_filters(mpctx, mediatype, cmd, arg); + if (on_osd) { + if (r >= 0) { + const char *prop = filter_opt[mediatype]; + show_property_osd(mpctx, prop, MP_ON_OSD_MSG); + } else { + set_osd_tmsg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration, + "Changing filters failed!"); + } + } + return r; +} + +static void change_video_filters(MPContext *mpctx, const char *cmd, + const char *arg) +{ + edit_filters(mpctx, STREAM_VIDEO, cmd, arg); +} + +void run_command(MPContext *mpctx, mp_cmd_t *cmd) +{ + struct MPOpts *opts = mpctx->opts; + sh_video_t *const sh_video = mpctx->sh_video; + int osd_duration = opts->osd_duration; + bool auto_osd = cmd->on_osd == MP_ON_OSD_AUTO; + bool msg_osd = auto_osd || (cmd->on_osd & MP_ON_OSD_MSG); + bool bar_osd = auto_osd || (cmd->on_osd & MP_ON_OSD_BAR); + int osdl = msg_osd ? 1 : OSD_LEVEL_INVISIBLE; + + if (!cmd->raw_args) { + for (int n = 0; n < cmd->nargs; n++) { + if (cmd->args[n].type.type == CONF_TYPE_STRING) { + cmd->args[n].v.s = + mp_property_expand_string(mpctx, cmd->args[n].v.s); + if (!cmd->args[n].v.s) + return; + talloc_steal(cmd, cmd->args[n].v.s); + } + } + } + + switch (cmd->id) { + case MP_CMD_SEEK: { + double v = cmd->args[0].v.d; + int abs = cmd->args[1].v.i; + int exact = cmd->args[2].v.i; + if (abs == 2) { // Absolute seek to a timestamp in seconds + queue_seek(mpctx, MPSEEK_ABSOLUTE, v, exact); + set_osd_function(mpctx, + v > get_current_time(mpctx) ? OSD_FFW : OSD_REW); + } else if (abs) { /* Absolute seek by percentage */ + queue_seek(mpctx, MPSEEK_FACTOR, v / 100.0, exact); + set_osd_function(mpctx, OSD_FFW); // Direction isn't set correctly + } else { + queue_seek(mpctx, MPSEEK_RELATIVE, v, exact); + set_osd_function(mpctx, (v > 0) ? OSD_FFW : OSD_REW); + } + if (bar_osd) + mpctx->add_osd_seek_info |= OSD_SEEK_INFO_BAR; + if (msg_osd && !(auto_osd && opts->osd_bar_visible)) + mpctx->add_osd_seek_info |= OSD_SEEK_INFO_TEXT; + break; + } + + case MP_CMD_SET: { + int r = mp_property_do(cmd->args[0].v.s, M_PROPERTY_SET_STRING, + cmd->args[1].v.s, mpctx); + if (r == M_PROPERTY_OK || r == M_PROPERTY_UNAVAILABLE) { + show_property_osd(mpctx, cmd->args[0].v.s, cmd->on_osd); + } else if (r == M_PROPERTY_UNKNOWN) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "Unknown property: '%s'\n", cmd->args[0].v.s); + } else if (r <= 0) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "Failed to set property '%s' to '%s'.\n", + cmd->args[0].v.s, cmd->args[1].v.s); + } + break; + } + + case MP_CMD_ADD: + case MP_CMD_CYCLE: + { + struct m_property_switch_arg s = { + .inc = 1, + .wrap = cmd->id == MP_CMD_CYCLE, + }; + if (cmd->args[1].v.d) + s.inc = cmd->args[1].v.d; + int r = mp_property_do(cmd->args[0].v.s, M_PROPERTY_SWITCH, &s, mpctx); + if (r == M_PROPERTY_OK || r == M_PROPERTY_UNAVAILABLE) { + show_property_osd(mpctx, cmd->args[0].v.s, cmd->on_osd); + } else if (r == M_PROPERTY_UNKNOWN) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "Unknown property: '%s'\n", cmd->args[0].v.s); + } else if (r <= 0) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "Failed to increment property '%s' by %g.\n", + cmd->args[0].v.s, s.inc); + } + break; + } + + case MP_CMD_GET_PROPERTY: { + char *tmp; + int r = mp_property_do(cmd->args[0].v.s, M_PROPERTY_GET_STRING, + &tmp, mpctx); + if (r <= 0) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "Failed to get value of property '%s'.\n", + cmd->args[0].v.s); + mp_msg(MSGT_GLOBAL, MSGL_INFO, "ANS_ERROR=%s\n", + property_error_string(r)); + break; + } + mp_msg(MSGT_GLOBAL, MSGL_INFO, "ANS_%s=%s\n", + cmd->args[0].v.s, tmp); + talloc_free(tmp); + break; + } + + case MP_CMD_SPEED_MULT: { + double v = cmd->args[0].v.d; + v *= mpctx->opts->playback_speed; + mp_property_do("speed", M_PROPERTY_SET, &v, mpctx); + show_property_osd(mpctx, "speed", cmd->on_osd); + break; + } + + case MP_CMD_FRAME_STEP: + add_step_frame(mpctx, 1); + break; + + case MP_CMD_FRAME_BACK_STEP: + add_step_frame(mpctx, -1); + break; + + case MP_CMD_QUIT: + mpctx->stop_play = PT_QUIT; + mpctx->quit_custom_rc = cmd->args[0].v.i; + mpctx->has_quit_custom_rc = true; + break; + + case MP_CMD_QUIT_WATCH_LATER: + mp_write_watch_later_conf(mpctx); + mpctx->stop_play = PT_QUIT; + mpctx->quit_player_rc = 0; + break; + + case MP_CMD_PLAYLIST_NEXT: + case MP_CMD_PLAYLIST_PREV: + { + int dir = cmd->id == MP_CMD_PLAYLIST_PREV ? -1 : +1; + int force = cmd->args[0].v.i; + + struct playlist_entry *e = mp_next_file(mpctx, dir); + if (!e && !force) + break; + mpctx->playlist->current = e; + mpctx->playlist->current_was_replaced = false; + mpctx->stop_play = PT_CURRENT_ENTRY; + break; + } + + case MP_CMD_SUB_STEP: + if (mpctx->osd->dec_sub) { + double a[2]; + a[0] = mpctx->video_pts - mpctx->osd->video_offset + opts->sub_delay; + a[1] = cmd->args[0].v.i; + if (sub_control(mpctx->osd->dec_sub, SD_CTRL_SUB_STEP, a) > 0) { + opts->sub_delay += a[0]; + + osd_changed_all(mpctx->osd); + set_osd_tmsg(mpctx, OSD_MSG_SUB_DELAY, osdl, osd_duration, + "Sub delay: %d ms", ROUND(opts->sub_delay * 1000)); + } + } + break; + + case MP_CMD_OSD: { + int v = cmd->args[0].v.i; + int max = (opts->term_osd + && !sh_video) ? MAX_TERM_OSD_LEVEL : MAX_OSD_LEVEL; + if (opts->osd_level > max) + opts->osd_level = max; + if (v < 0) + opts->osd_level = (opts->osd_level + 1) % (max + 1); + else + opts->osd_level = v > max ? max : v; + if (msg_osd && opts->osd_level <= 1) + set_osd_tmsg(mpctx, OSD_MSG_OSD_STATUS, 0, osd_duration, + "OSD: %s", opts->osd_level ? "yes" : "no"); + else + rm_osd_msg(mpctx, OSD_MSG_OSD_STATUS); + break; + } + + case MP_CMD_PRINT_TEXT: { + mp_msg(MSGT_GLOBAL, MSGL_INFO, "%s\n", cmd->args[0].v.s); + break; + } + + case MP_CMD_SHOW_TEXT: { + // if no argument supplied use default osd_duration, else <arg> ms. + set_osd_msg(mpctx, OSD_MSG_TEXT, cmd->args[2].v.i, + (cmd->args[1].v.i < 0 ? osd_duration : cmd->args[1].v.i), + "%s", cmd->args[0].v.s); + break; + } + + case MP_CMD_LOADFILE: { + char *filename = cmd->args[0].v.s; + bool append = cmd->args[1].v.i; + + if (!append) + playlist_clear(mpctx->playlist); + + playlist_add(mpctx->playlist, playlist_entry_new(filename)); + + if (!append) + mp_set_playlist_entry(mpctx, mpctx->playlist->first); + break; + } + + case MP_CMD_LOADLIST: { + char *filename = cmd->args[0].v.s; + bool append = cmd->args[1].v.i; + struct playlist *pl = playlist_parse_file(filename); + if (pl) { + if (!append) + playlist_clear(mpctx->playlist); + playlist_transfer_entries(mpctx->playlist, pl); + talloc_free(pl); + + if (!append && mpctx->playlist->first) + mp_set_playlist_entry(mpctx, mpctx->playlist->first); + } else { + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, + "\nUnable to load playlist %s.\n", filename); + } + break; + } + + case MP_CMD_PLAYLIST_CLEAR: { + // Supposed to clear the playlist, except the currently played item. + if (mpctx->playlist->current_was_replaced) + mpctx->playlist->current = NULL; + while (mpctx->playlist->first) { + struct playlist_entry *e = mpctx->playlist->first; + if (e == mpctx->playlist->current) { + e = e->next; + if (!e) + break; + } + playlist_remove(mpctx->playlist, e); + } + break; + } + + case MP_CMD_PLAYLIST_REMOVE: { + struct playlist_entry *e = playlist_entry_from_index(mpctx->playlist, + cmd->args[0].v.i); + if (e) { + // Can't play a removed entry + if (mpctx->playlist->current == e) + mpctx->stop_play = PT_CURRENT_ENTRY; + playlist_remove(mpctx->playlist, e); + } + break; + } + + case MP_CMD_PLAYLIST_MOVE: { + struct playlist_entry *e1 = playlist_entry_from_index(mpctx->playlist, + cmd->args[0].v.i); + struct playlist_entry *e2 = playlist_entry_from_index(mpctx->playlist, + cmd->args[1].v.i); + if (e1) { + playlist_move(mpctx->playlist, e1, e2); + } + break; + } + + case MP_CMD_STOP: + // Go back to the starting point. + mpctx->stop_play = PT_STOP; + break; + + case MP_CMD_SHOW_PROGRESS: + mpctx->add_osd_seek_info |= + (msg_osd ? OSD_SEEK_INFO_TEXT : 0) | + (bar_osd ? OSD_SEEK_INFO_BAR : 0); + break; + +#ifdef CONFIG_RADIO + case MP_CMD_RADIO_STEP_CHANNEL: + if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO) { + int v = cmd->args[0].v.i; + if (v > 0) + radio_step_channel(mpctx->stream, RADIO_CHANNEL_HIGHER); + else + radio_step_channel(mpctx->stream, RADIO_CHANNEL_LOWER); + if (radio_get_channel_name(mpctx->stream)) { + set_osd_tmsg(mpctx, OSD_MSG_RADIO_CHANNEL, osdl, osd_duration, + "Channel: %s", + radio_get_channel_name(mpctx->stream)); + } + } + break; + + case MP_CMD_RADIO_SET_CHANNEL: + if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO) { + radio_set_channel(mpctx->stream, cmd->args[0].v.s); + if (radio_get_channel_name(mpctx->stream)) { + set_osd_tmsg(mpctx, OSD_MSG_RADIO_CHANNEL, osdl, osd_duration, + "Channel: %s", + radio_get_channel_name(mpctx->stream)); + } + } + break; + + case MP_CMD_RADIO_SET_FREQ: + if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO) + radio_set_freq(mpctx->stream, cmd->args[0].v.f); + break; + + case MP_CMD_RADIO_STEP_FREQ: + if (mpctx->stream && mpctx->stream->type == STREAMTYPE_RADIO) + radio_step_freq(mpctx->stream, cmd->args[0].v.f); + break; +#endif + +#ifdef CONFIG_TV + case MP_CMD_TV_START_SCAN: + if (get_tvh(mpctx)) + tv_start_scan(get_tvh(mpctx), 1); + break; + case MP_CMD_TV_SET_FREQ: + if (get_tvh(mpctx)) + tv_set_freq(get_tvh(mpctx), cmd->args[0].v.f * 16.0); +#ifdef CONFIG_PVR + else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) { + pvr_set_freq(mpctx->stream, ROUND(cmd->args[0].v.f)); + set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s", + pvr_get_current_channelname(mpctx->stream), + pvr_get_current_stationname(mpctx->stream)); + } +#endif /* CONFIG_PVR */ + break; + + case MP_CMD_TV_STEP_FREQ: + if (get_tvh(mpctx)) + tv_step_freq(get_tvh(mpctx), cmd->args[0].v.f * 16.0); +#ifdef CONFIG_PVR + else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) { + pvr_force_freq_step(mpctx->stream, ROUND(cmd->args[0].v.f)); + set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: f %d", + pvr_get_current_channelname(mpctx->stream), + pvr_get_current_frequency(mpctx->stream)); + } +#endif /* CONFIG_PVR */ + break; + + case MP_CMD_TV_SET_NORM: + if (get_tvh(mpctx)) + tv_set_norm(get_tvh(mpctx), cmd->args[0].v.s); + break; + + case MP_CMD_TV_STEP_CHANNEL: + if (get_tvh(mpctx)) { + int v = cmd->args[0].v.i; + if (v > 0) { + tv_step_channel(get_tvh(mpctx), TV_CHANNEL_HIGHER); + } else { + tv_step_channel(get_tvh(mpctx), TV_CHANNEL_LOWER); + } + if (tv_channel_list) { + set_osd_tmsg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, + "Channel: %s", tv_channel_current->name); + } + } +#ifdef CONFIG_PVR + else if (mpctx->stream && + mpctx->stream->type == STREAMTYPE_PVR) { + pvr_set_channel_step(mpctx->stream, cmd->args[0].v.i); + set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s", + pvr_get_current_channelname(mpctx->stream), + pvr_get_current_stationname(mpctx->stream)); + } +#endif /* CONFIG_PVR */ +#ifdef CONFIG_DVBIN + if (mpctx->stream->type == STREAMTYPE_DVB) { + int dir; + int v = cmd->args[0].v.i; + + mpctx->last_dvb_step = v; + if (v > 0) + dir = DVB_CHANNEL_HIGHER; + else + dir = DVB_CHANNEL_LOWER; + + + if (dvb_step_channel(mpctx->stream, dir)) { + mpctx->stop_play = PT_NEXT_ENTRY; + mpctx->dvbin_reopen = 1; + } + } +#endif /* CONFIG_DVBIN */ + break; + + case MP_CMD_TV_SET_CHANNEL: + if (get_tvh(mpctx)) { + tv_set_channel(get_tvh(mpctx), cmd->args[0].v.s); + if (tv_channel_list) { + set_osd_tmsg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, + "Channel: %s", tv_channel_current->name); + } + } +#ifdef CONFIG_PVR + else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) { + pvr_set_channel(mpctx->stream, cmd->args[0].v.s); + set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s", + pvr_get_current_channelname(mpctx->stream), + pvr_get_current_stationname(mpctx->stream)); + } +#endif /* CONFIG_PVR */ + break; + +#ifdef CONFIG_DVBIN + case MP_CMD_DVB_SET_CHANNEL: + if (mpctx->stream->type == STREAMTYPE_DVB) { + mpctx->last_dvb_step = 1; + + if (dvb_set_channel(mpctx->stream, cmd->args[1].v.i, + cmd->args[0].v.i)) { + mpctx->stop_play = PT_NEXT_ENTRY; + mpctx->dvbin_reopen = 1; + } + } + break; +#endif /* CONFIG_DVBIN */ + + case MP_CMD_TV_LAST_CHANNEL: + if (get_tvh(mpctx)) { + tv_last_channel(get_tvh(mpctx)); + if (tv_channel_list) { + set_osd_tmsg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, + "Channel: %s", tv_channel_current->name); + } + } +#ifdef CONFIG_PVR + else if (mpctx->stream && mpctx->stream->type == STREAMTYPE_PVR) { + pvr_set_lastchannel(mpctx->stream); + set_osd_msg(mpctx, OSD_MSG_TV_CHANNEL, osdl, osd_duration, "%s: %s", + pvr_get_current_channelname(mpctx->stream), + pvr_get_current_stationname(mpctx->stream)); + } +#endif /* CONFIG_PVR */ + break; + + case MP_CMD_TV_STEP_NORM: + if (get_tvh(mpctx)) + tv_step_norm(get_tvh(mpctx)); + break; + + case MP_CMD_TV_STEP_CHANNEL_LIST: + if (get_tvh(mpctx)) + tv_step_chanlist(get_tvh(mpctx)); + break; +#endif /* CONFIG_TV */ + + case MP_CMD_SUB_ADD: + if (sh_video) { + mp_add_subtitles(mpctx, cmd->args[0].v.s, 0); + } + break; + + case MP_CMD_SUB_REMOVE: { + struct track *sub = mp_track_by_tid(mpctx, STREAM_SUB, cmd->args[0].v.i); + if (sub) + mp_remove_track(mpctx, sub); + break; + } + + case MP_CMD_SUB_RELOAD: { + struct track *sub = mp_track_by_tid(mpctx, STREAM_SUB, cmd->args[0].v.i); + if (sh_video && sub && sub->is_external && sub->external_filename) + { + struct track *nsub = mp_add_subtitles(mpctx, sub->external_filename, 0); + if (nsub) { + mp_remove_track(mpctx, sub); + mp_switch_track(mpctx, nsub->type, nsub); + } + } + break; + } + + case MP_CMD_SCREENSHOT: + screenshot_request(mpctx, cmd->args[0].v.i, cmd->args[1].v.i, msg_osd); + break; + + case MP_CMD_SCREENSHOT_TO_FILE: + screenshot_to_file(mpctx, cmd->args[0].v.s, cmd->args[1].v.i, msg_osd); + break; + + case MP_CMD_RUN: +#ifndef __MINGW32__ + if (!fork()) { + execl("/bin/sh", "sh", "-c", cmd->args[0].v.s, NULL); + exit(0); + } +#endif + break; + + case MP_CMD_KEYDOWN_EVENTS: + mp_input_put_key(mpctx->input, cmd->args[0].v.i); + break; + + case MP_CMD_ENABLE_INPUT_SECTION: + mp_input_enable_section(mpctx->input, cmd->args[0].v.s, + cmd->args[1].v.i == 1 ? MP_INPUT_EXCLUSIVE : 0); + break; + + case MP_CMD_DISABLE_INPUT_SECTION: + mp_input_disable_section(mpctx->input, cmd->args[0].v.s); + break; + + case MP_CMD_VO_CMDLINE: + if (mpctx->video_out) { + char *s = cmd->args[0].v.s; + mp_msg(MSGT_CPLAYER, MSGL_INFO, "Setting vo cmd line to '%s'.\n", + s); + if (vo_control(mpctx->video_out, VOCTRL_SET_COMMAND_LINE, s) > 0) { + set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration, "vo='%s'", s); + } else { + set_osd_msg(mpctx, OSD_MSG_TEXT, osdl, osd_duration, "Failed!"); + } + } + break; + + case MP_CMD_AF: + edit_filters_osd(mpctx, STREAM_AUDIO, cmd->args[0].v.s, + cmd->args[1].v.s, msg_osd); + break; + + case MP_CMD_VF: + edit_filters_osd(mpctx, STREAM_VIDEO, cmd->args[0].v.s, + cmd->args[1].v.s, msg_osd); + break; + + case MP_CMD_COMMAND_LIST: { + for (struct mp_cmd *sub = cmd->args[0].v.p; sub; sub = sub->queue_next) + run_command(mpctx, sub); + break; + } + + default: + mp_msg(MSGT_CPLAYER, MSGL_V, + "Received unknown cmd %s\n", cmd->name); + } + + switch (cmd->pausing) { + case 1: // "pausing" + pause_player(mpctx); + break; + case 3: // "pausing_toggle" + if (opts->pause) + unpause_player(mpctx); + else + pause_player(mpctx); + break; + } +} diff --git a/mpvcore/command.h b/mpvcore/command.h new file mode 100644 index 0000000000..dbe1f638e2 --- /dev/null +++ b/mpvcore/command.h @@ -0,0 +1,33 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_COMMAND_H +#define MPLAYER_COMMAND_H + +struct MPContext; +struct mp_cmd; + +void mp_get_osd_mouse_pos(struct MPContext *mpctx, float *x, float *y); + +void run_command(struct MPContext *mpctx, struct mp_cmd *cmd); +char *mp_property_expand_string(struct MPContext *mpctx, char *str); +void property_print_help(void); +int mp_property_do(const char* name, int action, void* val, + struct MPContext *mpctx); + +#endif /* MPLAYER_COMMAND_H */ diff --git a/mpvcore/cpudetect.c b/mpvcore/cpudetect.c new file mode 100644 index 0000000000..62cb03008d --- /dev/null +++ b/mpvcore/cpudetect.c @@ -0,0 +1,56 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdio.h> +#include <string.h> +#include <stdlib.h> + +#include <libavutil/cpu.h> +#include "compat/libav.h" + +#include "config.h" +#include "core/cpudetect.h" +#include "core/mp_msg.h" + +CpuCaps gCpuCaps; + +static void dump_flag(const char *name, bool val) +{ + mp_msg(MSGT_CPUDETECT, MSGL_V, "CPU: %s: %s\n", name, + val ? "enabled" : "disabled"); +} + +void GetCpuCaps(CpuCaps *c) +{ + memset(c, 0, sizeof(*c)); + int flags = av_get_cpu_flags(); +#if ARCH_X86 + c->hasMMX = flags & AV_CPU_FLAG_MMX; + c->hasMMX2 = flags & AV_CPU_FLAG_MMX2; + c->hasSSE = flags & AV_CPU_FLAG_SSE; + c->hasSSE2 = (flags & AV_CPU_FLAG_SSE2) && !(flags & AV_CPU_FLAG_SSE2SLOW); + c->hasSSE3 = (flags & AV_CPU_FLAG_SSE3) && !(flags & AV_CPU_FLAG_SSE3SLOW); + c->hasSSSE3 = flags & AV_CPU_FLAG_SSSE3; +#endif + dump_flag("MMX", c->hasMMX); + dump_flag("MMX2", c->hasMMX2); + dump_flag("SSE", c->hasSSE); + dump_flag("SSE2", c->hasSSE2); + dump_flag("SSE3", c->hasSSE3); + dump_flag("SSSE3", c->hasSSSE3); +} diff --git a/mpvcore/cpudetect.h b/mpvcore/cpudetect.h new file mode 100644 index 0000000000..d3d9206c65 --- /dev/null +++ b/mpvcore/cpudetect.h @@ -0,0 +1,40 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_CPUDETECT_H +#define MPLAYER_CPUDETECT_H + +#include <stdbool.h> +#include "config.h" + +#include "compat/x86_cpu.h" + +typedef struct cpucaps_s { + bool hasMMX; + bool hasMMX2; + bool hasSSE; + bool hasSSE2; + bool hasSSE3; + bool hasSSSE3; +} CpuCaps; + +extern CpuCaps gCpuCaps; + +void GetCpuCaps(CpuCaps *caps); + +#endif /* MPLAYER_CPUDETECT_H */ diff --git a/mpvcore/encode.h b/mpvcore/encode.h new file mode 100644 index 0000000000..acdb75c5a3 --- /dev/null +++ b/mpvcore/encode.h @@ -0,0 +1,22 @@ +#ifndef MPLAYER_ENCODE_H +#define MPLAYER_ENCODE_H + +#include <stdbool.h> +#include <libavutil/avutil.h> + +struct MPOpts; +struct encode_lavc_context; +struct encode_output_conf; + +// interface for mplayer.c +struct encode_lavc_context *encode_lavc_init(struct encode_output_conf *options); +void encode_lavc_finish(struct encode_lavc_context *ctx); +void encode_lavc_free(struct encode_lavc_context *ctx); +void encode_lavc_discontinuity(struct encode_lavc_context *ctx); +bool encode_lavc_showhelp(struct MPOpts *opts); +int encode_lavc_getstatus(struct encode_lavc_context *ctx, char *buf, int bufsize, float relative_position); +void encode_lavc_expect_stream(struct encode_lavc_context *ctx, enum AVMediaType mt); +void encode_lavc_set_video_fps(struct encode_lavc_context *ctx, float fps); +bool encode_lavc_didfail(struct encode_lavc_context *ctx); // check if encoding failed + +#endif diff --git a/mpvcore/encode_lavc.c b/mpvcore/encode_lavc.c new file mode 100644 index 0000000000..75e57a2443 --- /dev/null +++ b/mpvcore/encode_lavc.c @@ -0,0 +1,1115 @@ +/* + * muxing using libavformat + * Copyright (C) 2010 Nicolas George <george@nsup.org> + * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.org> + * + * This file is part of mpv. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + + +#include "encode_lavc.h" +#include "core/mp_msg.h" +#include "video/vfcap.h" +#include "core/options.h" +#include "osdep/timer.h" +#include "video/out/vo.h" +#include "talloc.h" +#include "stream/stream.h" + +static int set_to_avdictionary(AVDictionary **dictp, const char *key, + const char *val) +{ + char keybuf[1024]; + char valuebuf[1024]; + + if (key == NULL) { + // we need to split at equals sign + const char *equals = strchr(val, '='); + if (!equals || equals - val >= sizeof(keybuf)) { + mp_msg(MSGT_ENCODE, MSGL_WARN, + "encode-lavc: option '%s' does not contain an equals sign\n", + val); + return 0; + } + memcpy(keybuf, val, equals - val); + keybuf[equals - val] = 0; + key = keybuf; + val = equals + 1; + } + + // hack: support "qscale" key as virtual "global_quality" key that multiplies by QP2LAMBDA + if (!strcmp(key, "qscale")) { + key = "global_quality"; + snprintf(valuebuf, sizeof(valuebuf), + "%.1s(%s)*QP2LAMBDA", + (val[0] == '+' || val[0] == '-') ? val : "", + (val[0] == '+' || val[0] == '-') ? val + 1 : val); + valuebuf[sizeof(valuebuf) - 1] = 0; + val = valuebuf; + } + + mp_msg(MSGT_ENCODE, MSGL_V, + "encode-lavc: setting value '%s' for key '%s'\n", + val, + key); + + if (av_dict_set(dictp, key, *val ? val : NULL, + (val[0] == '+' || val[0] == '-') ? AV_DICT_APPEND : 0) >= 0) + return 1; + + return 0; +} + +static bool value_has_flag(const char *value, const char *flag) +{ + bool state = true; + bool ret = false; + while (*value) { + size_t l = strcspn(value, "+-"); + if (l == 0) { + state = (*value == '+'); + ++value; + } else { + if (l == strlen(flag)) + if (!memcmp(value, flag, l)) + ret = state; + value += l; + } + } + return ret; +} + +#define CHECK_FAIL(ctx, val) \ + if (ctx && (ctx->failed || ctx->finished)) { \ + mp_msg(MSGT_ENCODE, MSGL_ERR, \ + "Called a function on a %s encoding context. Bailing out.\n", \ + ctx->failed ? "failed" : "finished"); \ + return val; \ + } + +int encode_lavc_available(struct encode_lavc_context *ctx) +{ + CHECK_FAIL(ctx, 0); + return ctx && ctx->avc; +} + +int encode_lavc_oformat_flags(struct encode_lavc_context *ctx) +{ + CHECK_FAIL(ctx, 0); + return ctx->avc ? ctx->avc->oformat->flags : 0; +} + +struct encode_lavc_context *encode_lavc_init(struct encode_output_conf *options) +{ + struct encode_lavc_context *ctx; + const char *filename = options->file; + + // STUPID STUPID STUPID STUPID avio + // does not support "-" as file name to mean stdin/stdout + // ffmpeg.c works around this too, the same way + if (!strcmp(filename, "-")) + filename = "pipe:1"; + + if (filename && ( + !strcmp(filename, "/dev/stdout") || + !strcmp(filename, "pipe:") || + !strcmp(filename, "pipe:1"))) + mp_msg_stdout_in_use = 1; + + ctx = talloc_zero(NULL, struct encode_lavc_context); + encode_lavc_discontinuity(ctx); + ctx->options = options; + + ctx->avc = avformat_alloc_context(); + + if (ctx->options->format) { + char *tok; + const char *in = ctx->options->format; + while (*in) { + tok = av_get_token(&in, ","); + ctx->avc->oformat = av_guess_format(tok, filename, NULL); + av_free(tok); + if (ctx->avc->oformat) + break; + if (*in) + ++in; + } + } else + ctx->avc->oformat = av_guess_format(NULL, filename, NULL); + + if (!ctx->avc->oformat) { + encode_lavc_fail(ctx, "encode-lavc: format not found\n"); + return NULL; + } + + av_strlcpy(ctx->avc->filename, filename, + sizeof(ctx->avc->filename)); + + ctx->foptions = NULL; + if (ctx->options->fopts) { + char **p; + for (p = ctx->options->fopts; *p; ++p) { + if (!set_to_avdictionary(&ctx->foptions, NULL, *p)) + mp_msg(MSGT_ENCODE, MSGL_WARN, + "encode-lavc: could not set option %s\n", *p); + } + } + + if (ctx->options->vcodec) { + char *tok; + const char *in = ctx->options->vcodec; + while (*in) { + tok = av_get_token(&in, ","); + ctx->vc = avcodec_find_encoder_by_name(tok); + av_free(tok); + if (ctx->vc && ctx->vc->type != AVMEDIA_TYPE_VIDEO) + ctx->vc = NULL; + if (ctx->vc) + break; + if (*in) + ++in; + } + } else + ctx->vc = avcodec_find_encoder(av_guess_codec(ctx->avc->oformat, NULL, + ctx->avc->filename, NULL, + AVMEDIA_TYPE_VIDEO)); + + if (ctx->options->acodec) { + char *tok; + const char *in = ctx->options->acodec; + while (*in) { + tok = av_get_token(&in, ","); + ctx->ac = avcodec_find_encoder_by_name(tok); + av_free(tok); + if (ctx->ac && ctx->ac->type != AVMEDIA_TYPE_AUDIO) + ctx->ac = NULL; + if (ctx->ac) + break; + if (*in) + ++in; + } + } else + ctx->ac = avcodec_find_encoder(av_guess_codec(ctx->avc->oformat, NULL, + ctx->avc->filename, NULL, + AVMEDIA_TYPE_AUDIO)); + + if (!ctx->vc && !ctx->ac) { + encode_lavc_fail( + ctx, "encode-lavc: neither audio nor video codec was found\n"); + return NULL; + } + + /* taken from ffmpeg unchanged + * TODO turn this into an option if anyone needs this */ + + ctx->avc->max_delay = 0.7 * AV_TIME_BASE; + + ctx->abytes = 0; + ctx->vbytes = 0; + ctx->frames = 0; + + if (options->video_first) + ctx->video_first = true; + if (options->audio_first) + ctx->audio_first = true; + + return ctx; +} + +int encode_lavc_start(struct encode_lavc_context *ctx) +{ + AVDictionaryEntry *de; + unsigned i; + + if (ctx->header_written < 0) + return 0; + if (ctx->header_written > 0) + return 1; + + CHECK_FAIL(ctx, 0); + + if (ctx->expect_video) { + for (i = 0; i < ctx->avc->nb_streams; ++i) + if (ctx->avc->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO) + break; + if (i >= ctx->avc->nb_streams) { + encode_lavc_fail(ctx, + "encode-lavc: video stream missing, invalid codec?\n"); + return 0; + } + } + if (ctx->expect_audio) { + for (i = 0; i < ctx->avc->nb_streams; ++i) + if (ctx->avc->streams[i]->codec->codec_type == AVMEDIA_TYPE_AUDIO) + break; + if (i >= ctx->avc->nb_streams) { + encode_lavc_fail(ctx, + "encode-lavc: audio stream missing, invalid codec?\n"); + return 0; + } + } + + ctx->header_written = -1; + + if (!(ctx->avc->oformat->flags & AVFMT_NOFILE)) { + mp_msg(MSGT_ENCODE, MSGL_INFO, "Opening output file: %s\n", + ctx->avc->filename); + + if (avio_open(&ctx->avc->pb, ctx->avc->filename, + AVIO_FLAG_WRITE) < 0) { + encode_lavc_fail(ctx, "encode-lavc: could not open '%s'\n", + ctx->avc->filename); + return 0; + } + } + + ctx->t0 = mp_time_sec(); + + mp_msg(MSGT_ENCODE, MSGL_INFO, "Opening muxer: %s [%s]\n", + ctx->avc->oformat->long_name, ctx->avc->oformat->name); + + if (avformat_write_header(ctx->avc, &ctx->foptions) < 0) { + encode_lavc_fail(ctx, "encode-lavc: could not write header\n"); + return 0; + } + + for (de = NULL; (de = av_dict_get(ctx->foptions, "", de, + AV_DICT_IGNORE_SUFFIX));) + mp_msg(MSGT_ENCODE, MSGL_WARN, "ofopts: key '%s' not found.\n", de->key); + av_dict_free(&ctx->foptions); + + ctx->header_written = 1; + return 1; +} + +void encode_lavc_free(struct encode_lavc_context *ctx) +{ + if (!ctx) + return; + + if (!ctx->finished) + encode_lavc_fail(ctx, + "called encode_lavc_free without encode_lavc_finish\n"); + + talloc_free(ctx); +} + +void encode_lavc_finish(struct encode_lavc_context *ctx) +{ + unsigned i; + + if (!ctx) + return; + + if (ctx->finished) + return; + + if (ctx->avc) { + if (ctx->header_written > 0) + av_write_trailer(ctx->avc); // this is allowed to fail + + for (i = 0; i < ctx->avc->nb_streams; i++) { + switch (ctx->avc->streams[i]->codec->codec_type) { + case AVMEDIA_TYPE_VIDEO: + if (ctx->twopass_bytebuffer_v) { + char *stats = ctx->avc->streams[i]->codec->stats_out; + if (stats) + stream_write_buffer(ctx->twopass_bytebuffer_v, + stats, strlen(stats)); + } + break; + case AVMEDIA_TYPE_AUDIO: + if (ctx->twopass_bytebuffer_a) { + char *stats = ctx->avc->streams[i]->codec->stats_out; + if (stats) + stream_write_buffer(ctx->twopass_bytebuffer_a, + stats, strlen(stats)); + } + break; + default: + break; + } + avcodec_close(ctx->avc->streams[i]->codec); + talloc_free(ctx->avc->streams[i]->codec->stats_in); + av_free(ctx->avc->streams[i]->codec); + av_free(ctx->avc->streams[i]->info); + av_free(ctx->avc->streams[i]); + } + + if (ctx->twopass_bytebuffer_v) { + free_stream(ctx->twopass_bytebuffer_v); + ctx->twopass_bytebuffer_v = NULL; + } + + if (ctx->twopass_bytebuffer_a) { + free_stream(ctx->twopass_bytebuffer_a); + ctx->twopass_bytebuffer_a = NULL; + } + + mp_msg(MSGT_ENCODE, MSGL_INFO, "vo-lavc: encoded %lld bytes\n", + ctx->vbytes); + mp_msg(MSGT_ENCODE, MSGL_INFO, "ao-lavc: encoded %lld bytes\n", + ctx->abytes); + if (ctx->avc->pb) { + mp_msg(MSGT_ENCODE, MSGL_INFO, + "encode-lavc: muxing overhead %lld bytes\n", + (long long) (avio_size(ctx->avc->pb) - ctx->vbytes + - ctx->abytes)); + avio_close(ctx->avc->pb); + } + + av_free(ctx->avc); + } + + ctx->finished = true; +} + +void encode_lavc_set_video_fps(struct encode_lavc_context *ctx, float fps) +{ + ctx->vo_fps = fps; +} + +static void encode_2pass_prepare(struct encode_lavc_context *ctx, + AVDictionary **dictp, + AVStream *stream, struct stream **bytebuf, + const char *prefix) +{ + if (!*bytebuf) { + char buf[sizeof(ctx->avc->filename) + 12]; + AVDictionaryEntry *de = av_dict_get(ctx->voptions, "flags", NULL, 0); + + snprintf(buf, sizeof(buf), "%s-%s-pass1.log", ctx->avc->filename, + prefix); + buf[sizeof(buf) - 1] = 0; + + if (value_has_flag(de ? de->value : "", "pass2")) { + if (!(*bytebuf = stream_open(buf, NULL))) { + mp_msg(MSGT_ENCODE, MSGL_WARN, "%s: could not open '%s', " + "disabling 2-pass encoding at pass 2\n", prefix, buf); + stream->codec->flags &= ~CODEC_FLAG_PASS2; + set_to_avdictionary(dictp, "flags", "-pass2"); + } else { + struct bstr content = stream_read_complete(*bytebuf, NULL, + 1000000000); + if (content.start == NULL) { + mp_msg(MSGT_ENCODE, MSGL_WARN, "%s: could not read '%s', " + "disabling 2-pass encoding at pass 1\n", + prefix, ctx->avc->filename); + } else { + content.start[content.len] = 0; + stream->codec->stats_in = content.start; + } + free_stream(*bytebuf); + *bytebuf = NULL; + } + } + + if (value_has_flag(de ? de->value : "", "pass1")) { + if (!(*bytebuf = open_output_stream(buf, NULL))) { + mp_msg( + MSGT_ENCODE, MSGL_WARN, + "%s: could not open '%s', disabling " + "2-pass encoding at pass 1\n", + prefix, ctx->avc->filename); + set_to_avdictionary(dictp, "flags", "-pass1"); + } + } + } +} + +AVStream *encode_lavc_alloc_stream(struct encode_lavc_context *ctx, + enum AVMediaType mt) +{ + AVDictionaryEntry *de; + AVStream *stream = NULL; + char **p; + int i; + + CHECK_FAIL(ctx, NULL); + + if (ctx->header_written) + return NULL; + + for (i = 0; i < ctx->avc->nb_streams; ++i) + if (ctx->avc->streams[i]->codec->codec_type == mt) + // already have a stream of that type, this cannot really happen + return NULL; + + if (ctx->avc->nb_streams == 0) { + // if this stream isn't stream #0, allocate a dummy stream first for + // the next loop to use + if (mt == AVMEDIA_TYPE_VIDEO && ctx->audio_first) { + mp_msg(MSGT_ENCODE, MSGL_INFO, + "vo-lavc: preallocated audio stream for later use\n"); + avformat_new_stream(ctx->avc, NULL); // this one is AVMEDIA_TYPE_UNKNOWN for now + } + if (mt == AVMEDIA_TYPE_AUDIO && ctx->video_first) { + mp_msg(MSGT_ENCODE, MSGL_INFO, + "ao-lavc: preallocated video stream for later use\n"); + avformat_new_stream(ctx->avc, NULL); // this one is AVMEDIA_TYPE_UNKNOWN for now + } + } else { + // find possibly preallocated stream + for (i = 0; i < ctx->avc->nb_streams; ++i) + if (ctx->avc->streams[i]->codec->codec_type == AVMEDIA_TYPE_UNKNOWN) // preallocated stream + stream = ctx->avc->streams[i]; + } + if (!stream) + stream = avformat_new_stream(ctx->avc, NULL); + + if (ctx->timebase.den == 0) { + AVRational r; + + if (ctx->options->fps > 0) + r = av_d2q(ctx->options->fps, ctx->options->fps * 1001 + 2); + else if (ctx->options->autofps && ctx->vo_fps > 0) { + r = av_d2q(ctx->vo_fps, ctx->vo_fps * 1001 + 2); + mp_msg( + MSGT_ENCODE, MSGL_INFO, "vo-lavc: option --ofps not specified " + "but --oautofps is active, using guess of %u/%u\n", + (unsigned)r.num, (unsigned)r.den); + } else { + // we want to handle: + // 1/25 + // 1001/24000 + // 1001/30000 + // for this we would need 120000fps... + // however, mpeg-4 only allows 16bit values + // so let's take 1001/30000 out + r.num = 24000; + r.den = 1; + mp_msg( + MSGT_ENCODE, MSGL_INFO, "vo-lavc: option --ofps not specified " + "and fps could not be inferred, using guess of %u/%u\n", + (unsigned)r.num, (unsigned)r.den); + } + + if (ctx->vc && ctx->vc->supported_framerates) + r = ctx->vc->supported_framerates[av_find_nearest_q_idx(r, + ctx->vc->supported_framerates)]; + + ctx->timebase.num = r.den; + ctx->timebase.den = r.num; + } + + switch (mt) { + case AVMEDIA_TYPE_VIDEO: + if (!ctx->vc) { + encode_lavc_fail(ctx, "vo-lavc: encoder not found\n"); + return NULL; + } + avcodec_get_context_defaults3(stream->codec, ctx->vc); + + // stream->time_base = ctx->timebase; + // doing this breaks mpeg2ts in ffmpeg + // which doesn't properly force the time base to be 90000 + // furthermore, ffmpeg.c doesn't do this either and works + + stream->codec->time_base = ctx->timebase; + + ctx->voptions = NULL; + + if (ctx->options->vopts) + for (p = ctx->options->vopts; *p; ++p) + if (!set_to_avdictionary(&ctx->voptions, NULL, *p)) + mp_msg(MSGT_ENCODE, MSGL_WARN, + "vo-lavc: could not set option %s\n", *p); + + de = av_dict_get(ctx->voptions, "global_quality", NULL, 0); + if (de) + set_to_avdictionary(&ctx->voptions, "flags", "+qscale"); + + if (ctx->avc->oformat->flags & AVFMT_GLOBALHEADER) + set_to_avdictionary(&ctx->voptions, "flags", "+global_header"); + + encode_2pass_prepare(ctx, &ctx->voptions, stream, + &ctx->twopass_bytebuffer_v, + "vo-lavc"); + break; + + case AVMEDIA_TYPE_AUDIO: + if (!ctx->ac) { + encode_lavc_fail(ctx, "ao-lavc: encoder not found\n"); + return NULL; + } + avcodec_get_context_defaults3(stream->codec, ctx->ac); + + stream->codec->time_base = ctx->timebase; + + ctx->aoptions = NULL; + + if (ctx->options->aopts) + for (p = ctx->options->aopts; *p; ++p) + if (!set_to_avdictionary(&ctx->aoptions, NULL, *p)) + mp_msg(MSGT_ENCODE, MSGL_WARN, + "ao-lavc: could not set option %s\n", *p); + + de = av_dict_get(ctx->aoptions, "global_quality", NULL, 0); + if (de) + set_to_avdictionary(&ctx->aoptions, "flags", "+qscale"); + + if (ctx->avc->oformat->flags & AVFMT_GLOBALHEADER) + set_to_avdictionary(&ctx->aoptions, "flags", "+global_header"); + + encode_2pass_prepare(ctx, &ctx->aoptions, stream, + &ctx->twopass_bytebuffer_a, + "ao-lavc"); + break; + + default: + encode_lavc_fail(ctx, "encode-lavc: requested invalid stream type\n"); + return NULL; + } + + return stream; +} + +AVCodec *encode_lavc_get_codec(struct encode_lavc_context *ctx, + AVStream *stream) +{ + CHECK_FAIL(ctx, NULL); + + switch (stream->codec->codec_type) { + case AVMEDIA_TYPE_VIDEO: + return ctx->vc; + case AVMEDIA_TYPE_AUDIO: + return ctx->ac; + default: + break; + } + return NULL; +} + +int encode_lavc_open_codec(struct encode_lavc_context *ctx, AVStream *stream) +{ + AVDictionaryEntry *de; + int ret; + + CHECK_FAIL(ctx, -1); + + switch (stream->codec->codec_type) { + case AVMEDIA_TYPE_VIDEO: + mp_msg(MSGT_ENCODE, MSGL_INFO, "Opening video encoder: %s [%s]\n", + ctx->vc->long_name, ctx->vc->name); + + if (ctx->vc->capabilities & CODEC_CAP_EXPERIMENTAL) { + stream->codec->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; + mp_msg(MSGT_ENCODE, MSGL_WARN, _( + "\n\n" + " ********************************************\n" + " **** Experimental VIDEO codec selected! ****\n" + " ********************************************\n\n" + "This means the output file may be broken or bad.\n" + "Possible reasons, problems, workarounds:\n" + "- Codec implementation in ffmpeg/libav is not finished yet.\n" + " Try updating ffmpeg or libav.\n" + "- Bad picture quality, blocks, blurriness.\n" + " Experiment with codec settings (--ovcopts) to maybe still get the\n" + " desired quality output at the expense of bitrate.\n" + "- Slow compression.\n" + " Bear with it.\n" + "- Crashes.\n" + " Happens. Try varying options to work around.\n" + "If none of this helps you, try another codec in place of %s.\n\n"), + ctx->vc->name); + } + + ret = avcodec_open2(stream->codec, ctx->vc, &ctx->voptions); + + // complain about all remaining options, then free the dict + for (de = NULL; (de = av_dict_get(ctx->voptions, "", de, + AV_DICT_IGNORE_SUFFIX));) + mp_msg(MSGT_ENCODE, MSGL_WARN, "ovcopts: key '%s' not found.\n", + de->key); + av_dict_free(&ctx->voptions); + + break; + case AVMEDIA_TYPE_AUDIO: + mp_msg(MSGT_ENCODE, MSGL_INFO, "Opening audio encoder: %s [%s]\n", + ctx->ac->long_name, ctx->ac->name); + + if (ctx->ac->capabilities & CODEC_CAP_EXPERIMENTAL) { + stream->codec->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; + mp_msg(MSGT_ENCODE, MSGL_WARN, _( + "\n\n" + " ********************************************\n" + " **** Experimental AUDIO codec selected! ****\n" + " ********************************************\n\n" + "This means the output file may be broken or bad.\n" + "Possible reasons, problems, workarounds:\n" + "- Codec implementation in ffmpeg/libav is not finished yet.\n" + " Try updating ffmpeg or libav.\n" + "- Bad sound quality, noise, clicking, whistles, choppiness.\n" + " Experiment with codec settings (--oacopts) to maybe still get the\n" + " desired quality output at the expense of bitrate.\n" + "- Slow compression.\n" + " Bear with it.\n" + "- Crashes.\n" + " Happens. Try varying options to work around.\n" + "If none of this helps you, try another codec in place of %s.\n\n"), + ctx->ac->name); + } + ret = avcodec_open2(stream->codec, ctx->ac, &ctx->aoptions); + + // complain about all remaining options, then free the dict + for (de = NULL; (de = av_dict_get(ctx->aoptions, "", de, + AV_DICT_IGNORE_SUFFIX));) + mp_msg(MSGT_ENCODE, MSGL_WARN, "oacopts: key '%s' not found.\n", + de->key); + av_dict_free(&ctx->aoptions); + + break; + default: + ret = -1; + break; + } + + if (ret < 0) + encode_lavc_fail(ctx, + "unable to open encoder (see above for the cause)"); + + return ret; +} + +void encode_lavc_write_stats(struct encode_lavc_context *ctx, AVStream *stream) +{ + CHECK_FAIL(ctx, ); + + switch (stream->codec->codec_type) { + case AVMEDIA_TYPE_VIDEO: + if (ctx->twopass_bytebuffer_v) + if (stream->codec->stats_out) + stream_write_buffer(ctx->twopass_bytebuffer_v, + stream->codec->stats_out, + strlen(stream->codec->stats_out)); + break; + case AVMEDIA_TYPE_AUDIO: + if (ctx->twopass_bytebuffer_a) + if (stream->codec->stats_out) + stream_write_buffer(ctx->twopass_bytebuffer_a, + stream->codec->stats_out, + strlen(stream->codec->stats_out)); + break; + default: + break; + } +} + +int encode_lavc_write_frame(struct encode_lavc_context *ctx, AVPacket *packet) +{ + int r; + + CHECK_FAIL(ctx, -1); + + if (ctx->header_written <= 0) + return -1; + + mp_msg( + MSGT_ENCODE, MSGL_DBG2, + "encode-lavc: write frame: stream %d ptsi %d (%f) dtsi %d (%f) size %d\n", + (int)packet->stream_index, + (int)packet->pts, + packet->pts + * (double)ctx->avc->streams[packet->stream_index]->time_base.num + / (double)ctx->avc->streams[packet->stream_index]->time_base.den, + (int)packet->dts, + packet->dts + * (double)ctx->avc->streams[packet->stream_index]->time_base.num + / (double)ctx->avc->streams[packet->stream_index]->time_base.den, + (int)packet->size); + + switch (ctx->avc->streams[packet->stream_index]->codec->codec_type) { + case AVMEDIA_TYPE_VIDEO: + ctx->vbytes += packet->size; + ++ctx->frames; + break; + case AVMEDIA_TYPE_AUDIO: + ctx->abytes += packet->size; + ctx->audioseconds += packet->duration + * (double)ctx->avc->streams[packet->stream_index]->time_base.num + / (double)ctx->avc->streams[packet->stream_index]->time_base.den; + break; + default: + break; + } + + r = av_interleaved_write_frame(ctx->avc, packet); + + return r; +} + +int encode_lavc_supports_pixfmt(struct encode_lavc_context *ctx, + enum PixelFormat pix_fmt) +{ + CHECK_FAIL(ctx, 0); + + if (!ctx->vc) + return 0; + if (pix_fmt == PIX_FMT_NONE) + return 0; + + if (!ctx->vc->pix_fmts) + return VFCAP_CSP_SUPPORTED; + else { + const enum PixelFormat *p; + for (p = ctx->vc->pix_fmts; *p >= 0; ++p) { + if (pix_fmt == *p) + return VFCAP_CSP_SUPPORTED; + } + } + return 0; +} + +void encode_lavc_discontinuity(struct encode_lavc_context *ctx) +{ + if (!ctx) + return; + + CHECK_FAIL(ctx, ); + + ctx->audio_pts_offset = MP_NOPTS_VALUE; + ctx->last_video_in_pts = MP_NOPTS_VALUE; + ctx->discontinuity_pts_offset = MP_NOPTS_VALUE; +} + +static void encode_lavc_printoptions(void *obj, const char *indent, + const char *subindent, const char *unit, + int filter_and, int filter_eq) +{ + const AVOption *opt = NULL; + char optbuf[32]; + while ((opt = av_opt_next(obj, opt))) { + // if flags are 0, it simply hasn't been filled in yet and may be + // potentially useful + if (opt->flags) + if ((opt->flags & filter_and) != filter_eq) + continue; + /* Don't print CONST's on level one. + * Don't print anything but CONST's on level two. + * Only print items from the requested unit. + */ + if (!unit && opt->type == AV_OPT_TYPE_CONST) + continue; + else if (unit && opt->type != AV_OPT_TYPE_CONST) + continue; + else if (unit && opt->type == AV_OPT_TYPE_CONST + && strcmp(unit, opt->unit)) + continue; + else if (unit && opt->type == AV_OPT_TYPE_CONST) + mp_msg(MSGT_ENCODE, MSGL_INFO, "%s", subindent); + else + mp_msg(MSGT_ENCODE, MSGL_INFO, "%s", indent); + + switch (opt->type) { + case AV_OPT_TYPE_FLAGS: + snprintf(optbuf, sizeof(optbuf), "%s=<flags>", opt->name); + break; + case AV_OPT_TYPE_INT: + snprintf(optbuf, sizeof(optbuf), "%s=<int>", opt->name); + break; + case AV_OPT_TYPE_INT64: + snprintf(optbuf, sizeof(optbuf), "%s=<int64>", opt->name); + break; + case AV_OPT_TYPE_DOUBLE: + snprintf(optbuf, sizeof(optbuf), "%s=<double>", opt->name); + break; + case AV_OPT_TYPE_FLOAT: + snprintf(optbuf, sizeof(optbuf), "%s=<float>", opt->name); + break; + case AV_OPT_TYPE_STRING: + snprintf(optbuf, sizeof(optbuf), "%s=<string>", opt->name); + break; + case AV_OPT_TYPE_RATIONAL: + snprintf(optbuf, sizeof(optbuf), "%s=<rational>", opt->name); + break; + case AV_OPT_TYPE_BINARY: + snprintf(optbuf, sizeof(optbuf), "%s=<binary>", opt->name); + break; + case AV_OPT_TYPE_CONST: + snprintf(optbuf, sizeof(optbuf), " [+-]%s", opt->name); + break; + default: + snprintf(optbuf, sizeof(optbuf), "%s", opt->name); + break; + } + optbuf[sizeof(optbuf) - 1] = 0; + mp_msg(MSGT_ENCODE, MSGL_INFO, "%-32s ", optbuf); + if (opt->help) + mp_msg(MSGT_ENCODE, MSGL_INFO, " %s", opt->help); + mp_msg(MSGT_ENCODE, MSGL_INFO, "\n"); + if (opt->unit && opt->type != AV_OPT_TYPE_CONST) + encode_lavc_printoptions(obj, indent, subindent, opt->unit, + filter_and, filter_eq); + } +} + +bool encode_lavc_showhelp(struct MPOpts *opts) +{ + bool help_output = false; + if (av_codec_next(NULL) == NULL) + mp_msg(MSGT_ENCODE, MSGL_ERR, "NO CODECS\n"); +#define CHECKS(str) ((str) && \ + strcmp((str), "help") == 0 ? (help_output |= 1) : 0) +#define CHECKV(strv) ((strv) && (strv)[0] && \ + strcmp((strv)[0], "help") == 0 ? (help_output |= 1) : 0) + if (CHECKS(opts->encode_output.format)) { + AVOutputFormat *c = NULL; + mp_msg(MSGT_ENCODE, MSGL_INFO, "Available output formats:\n"); + while ((c = av_oformat_next(c))) + mp_msg(MSGT_ENCODE, MSGL_INFO, " --of=%-13s %s\n", c->name, + c->long_name ? c->long_name : ""); + av_free(c); + } + if (CHECKV(opts->encode_output.fopts)) { + AVFormatContext *c = avformat_alloc_context(); + AVOutputFormat *format = NULL; + mp_msg(MSGT_ENCODE, MSGL_INFO, + "Available output format ctx->options:\n"); + encode_lavc_printoptions(c, " --ofopts=", " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM, + AV_OPT_FLAG_ENCODING_PARAM); + av_free(c); + while ((format = av_oformat_next(format))) { + if (format->priv_class) { + mp_msg(MSGT_ENCODE, MSGL_INFO, "Additionally, for --of=%s:\n", + format->name); + encode_lavc_printoptions(&format->priv_class, " --ofopts=", + " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM, + AV_OPT_FLAG_ENCODING_PARAM); + } + } + } + if (CHECKV(opts->encode_output.vopts)) { + AVCodecContext *c = avcodec_alloc_context3(NULL); + AVCodec *codec = NULL; + mp_msg(MSGT_ENCODE, MSGL_INFO, + "Available output video codec ctx->options:\n"); + encode_lavc_printoptions( + c, " --ovcopts=", " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM); + av_free(c); + while ((codec = av_codec_next(codec))) { + if (!av_codec_is_encoder(codec)) + continue; + if (codec->type != AVMEDIA_TYPE_VIDEO) + continue; + if (opts->encode_output.vcodec && opts->encode_output.vcodec[0] && + strcmp(opts->encode_output.vcodec, codec->name) != 0) + continue; + if (codec->priv_class) { + mp_msg(MSGT_ENCODE, MSGL_INFO, "Additionally, for --ovc=%s:\n", + codec->name); + encode_lavc_printoptions( + &codec->priv_class, " --ovcopts=", + " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM); + } + } + } + if (CHECKV(opts->encode_output.aopts)) { + AVCodecContext *c = avcodec_alloc_context3(NULL); + AVCodec *codec = NULL; + mp_msg(MSGT_ENCODE, MSGL_INFO, + "Available output audio codec ctx->options:\n"); + encode_lavc_printoptions( + c, " --oacopts=", " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM); + av_free(c); + while ((codec = av_codec_next(codec))) { + if (!av_codec_is_encoder(codec)) + continue; + if (codec->type != AVMEDIA_TYPE_AUDIO) + continue; + if (opts->encode_output.acodec && opts->encode_output.acodec[0] && + strcmp(opts->encode_output.acodec, codec->name) != 0) + continue; + if (codec->priv_class) { + mp_msg(MSGT_ENCODE, MSGL_INFO, "Additionally, for --oac=%s:\n", + codec->name); + encode_lavc_printoptions( + &codec->priv_class, " --oacopts=", + " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM); + } + } + } + if (CHECKS(opts->encode_output.vcodec)) { + AVCodec *c = NULL; + mp_msg(MSGT_ENCODE, MSGL_INFO, "Available output video codecs:\n"); + while ((c = av_codec_next(c))) { + if (!av_codec_is_encoder(c)) + continue; + if (c->type != AVMEDIA_TYPE_VIDEO) + continue; + mp_msg(MSGT_ENCODE, MSGL_INFO, " --ovc=%-12s %s\n", c->name, + c->long_name ? c->long_name : ""); + } + av_free(c); + } + if (CHECKS(opts->encode_output.acodec)) { + AVCodec *c = NULL; + mp_msg(MSGT_ENCODE, MSGL_INFO, "Available output audio codecs:\n"); + while ((c = av_codec_next(c))) { + if (!av_codec_is_encoder(c)) + continue; + if (c->type != AVMEDIA_TYPE_AUDIO) + continue; + mp_msg(MSGT_ENCODE, MSGL_INFO, " --oac=%-12s %s\n", c->name, + c->long_name ? c->long_name : ""); + } + av_free(c); + } + return help_output; +} + +double encode_lavc_getoffset(struct encode_lavc_context *ctx, AVStream *stream) +{ + CHECK_FAIL(ctx, 0); + + switch (stream->codec->codec_type) { + case AVMEDIA_TYPE_VIDEO: + return ctx->options->voffset; + case AVMEDIA_TYPE_AUDIO: + return ctx->options->aoffset; + default: + break; + } + return 0; +} + +int encode_lavc_getstatus(struct encode_lavc_context *ctx, + char *buf, int bufsize, + float relative_position) +{ + double now = mp_time_sec(); + float minutes, megabytes, fps, x; + float f = FFMAX(0.0001, relative_position); + if (!ctx) + return -1; + + CHECK_FAIL(ctx, -1); + + minutes = (now - ctx->t0) / 60.0 * (1 - f) / f; + megabytes = ctx->avc->pb ? (avio_size(ctx->avc->pb) / 1048576.0 / f) : 0; + fps = ctx->frames / (now - ctx->t0); + x = ctx->audioseconds / (now - ctx->t0); + if (ctx->frames) + snprintf(buf, bufsize, "{%.1fmin %.1ffps %.1fMB}", + minutes, fps, megabytes); + else if (ctx->audioseconds) + snprintf(buf, bufsize, "{%.1fmin %.2fx %.1fMB}", + minutes, x, megabytes); + else + snprintf(buf, bufsize, "{%.1fmin %.1fMB}", + minutes, megabytes); + buf[bufsize - 1] = 0; + return 0; +} + +void encode_lavc_expect_stream(struct encode_lavc_context *ctx, + enum AVMediaType mt) +{ + CHECK_FAIL(ctx, ); + + switch (mt) { + case AVMEDIA_TYPE_VIDEO: + ctx->expect_video = true; + break; + case AVMEDIA_TYPE_AUDIO: + ctx->expect_audio = true; + break; + } +} + +bool encode_lavc_didfail(struct encode_lavc_context *ctx) +{ + return ctx && ctx->failed; +} + +void encode_lavc_fail(struct encode_lavc_context *ctx, const char *format, ...) +{ + va_list va; + va_start(va, format); + mp_msg_va(MSGT_ENCODE, MSGL_ERR, format, va); + if (ctx->failed) + return; + ctx->failed = true; + encode_lavc_finish(ctx); +} + +bool encode_lavc_set_csp(struct encode_lavc_context *ctx, + AVStream *stream, enum mp_csp csp) +{ + CHECK_FAIL(ctx, NULL); + + if (ctx->header_written) { + if (stream->codec->colorspace != mp_csp_to_avcol_spc(csp)) + mp_msg(MSGT_ENCODE, MSGL_WARN, + "encode-lavc: can not change color space during encoding\n"); + return false; + } + + stream->codec->colorspace = mp_csp_to_avcol_spc(csp); + return true; +} + +bool encode_lavc_set_csp_levels(struct encode_lavc_context *ctx, + AVStream *stream, enum mp_csp_levels lev) +{ + CHECK_FAIL(ctx, NULL); + + if (ctx->header_written) { + if (stream->codec->color_range != mp_csp_levels_to_avcol_range(lev)) + mp_msg(MSGT_ENCODE, MSGL_WARN, + "encode-lavc: can not change color space during encoding\n"); + return false; + } + + stream->codec->color_range = mp_csp_levels_to_avcol_range(lev); + return true; +} + +enum mp_csp encode_lavc_get_csp(struct encode_lavc_context *ctx, + AVStream *stream) +{ + CHECK_FAIL(ctx, 0); + + return avcol_spc_to_mp_csp(stream->codec->colorspace); +} + +enum mp_csp_levels encode_lavc_get_csp_levels(struct encode_lavc_context *ctx, + AVStream *stream) +{ + CHECK_FAIL(ctx, 0); + + return avcol_range_to_mp_csp_levels(stream->codec->color_range); +} + +// vim: ts=4 sw=4 et diff --git a/mpvcore/encode_lavc.h b/mpvcore/encode_lavc.h new file mode 100644 index 0000000000..f47825e1d7 --- /dev/null +++ b/mpvcore/encode_lavc.h @@ -0,0 +1,101 @@ +/* + * muxing using libavformat + * Copyright (C) 2011 Rudolf Polzer <divVerent@xonotic.org> + * + * This file is part of mpv. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_ENCODE_LAVC_H +#define MPLAYER_ENCODE_LAVC_H + +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libavutil/avstring.h> +#include <libavutil/pixfmt.h> +#include <libavutil/opt.h> +#include <libavutil/mathematics.h> + +#include "encode.h" +#include "video/csputils.h" + +struct encode_lavc_context { + struct encode_output_conf *options; + + float vo_fps; + + // these are processed from the options + AVFormatContext *avc; + AVRational timebase; + AVCodec *vc; + AVCodec *ac; + AVDictionary *foptions; + AVDictionary *aoptions; + AVDictionary *voptions; + + // values created during encoding + int header_written; // -1 means currently writing + + // sync to audio mode + double audio_pts_offset; + double last_video_in_pts; + + // anti discontinuity mode + double next_in_pts; + double discontinuity_pts_offset; + + long long abytes; + long long vbytes; + struct stream *twopass_bytebuffer_a; + struct stream *twopass_bytebuffer_v; + double t0; + unsigned int frames; + double audioseconds; + + bool expect_video; + bool expect_audio; + bool video_first; + bool audio_first; + + // has encoding failed? + bool failed; + bool finished; +}; + +// interface for vo/ao drivers +AVStream *encode_lavc_alloc_stream(struct encode_lavc_context *ctx, enum AVMediaType mt); +void encode_lavc_write_stats(struct encode_lavc_context *ctx, AVStream *stream); +int encode_lavc_write_frame(struct encode_lavc_context *ctx, AVPacket *packet); +int encode_lavc_supports_pixfmt(struct encode_lavc_context *ctx, enum PixelFormat format); +AVCodec *encode_lavc_get_codec(struct encode_lavc_context *ctx, AVStream *stream); +int encode_lavc_open_codec(struct encode_lavc_context *ctx, AVStream *stream); +int encode_lavc_available(struct encode_lavc_context *ctx); +int encode_lavc_timesyncfailed(struct encode_lavc_context *ctx); +int encode_lavc_start(struct encode_lavc_context *ctx); // returns 1 on success +int encode_lavc_oformat_flags(struct encode_lavc_context *ctx); +double encode_lavc_getoffset(struct encode_lavc_context *ctx, AVStream *stream); +void encode_lavc_fail(struct encode_lavc_context *ctx, const char *format, ...); // report failure of encoding + +bool encode_lavc_set_csp(struct encode_lavc_context *ctx, + AVStream *stream, enum mp_csp csp); +bool encode_lavc_set_csp_levels(struct encode_lavc_context *ctx, + AVStream *stream, enum mp_csp_levels lev); +enum mp_csp encode_lavc_get_csp(struct encode_lavc_context *ctx, + AVStream *stream); +enum mp_csp_levels encode_lavc_get_csp_levels(struct encode_lavc_context *ctx, + AVStream *stream); + +#endif diff --git a/mpvcore/input/input.c b/mpvcore/input/input.c new file mode 100644 index 0000000000..ae1358a76d --- /dev/null +++ b/mpvcore/input/input.c @@ -0,0 +1,2284 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <stdio.h> +#include <stdbool.h> +#include <unistd.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <sys/time.h> +#include <fcntl.h> +#include <ctype.h> +#include <assert.h> + +#include <libavutil/avstring.h> +#include <libavutil/common.h> + +#include "osdep/io.h" +#include "osdep/getch2.h" + +#include "input.h" +#include "keycodes.h" +#include "osdep/timer.h" +#include "core/mp_msg.h" +#include "core/m_config.h" +#include "core/m_option.h" +#include "core/path.h" +#include "talloc.h" +#include "core/options.h" +#include "core/bstr.h" +#include "stream/stream.h" +#include "core/mp_common.h" + +#include "joystick.h" + +#ifdef CONFIG_LIRC +#include "lirc.h" +#endif + +#ifdef CONFIG_LIRCC +#include <lirc/lircc.h> +#endif + +#ifdef CONFIG_COCOA +#include "osdep/macosx_events.h" +#endif + +#define MP_MAX_KEY_DOWN 4 + +struct cmd_bind { + int keys[MP_MAX_KEY_DOWN]; + int num_keys; + char *cmd; + char *location; // filename/line number of definition + bool is_builtin; + struct cmd_bind_section *owner; +}; + +struct key_name { + int key; + char *name; +}; + +/* This array defines all known commands. + * The first field is an id used to recognize the command. + * The second is the command name used in slave mode and input.conf. + * Then comes the definition of each argument, first mandatory arguments + * (ARG_INT, ARG_FLOAT, ARG_STRING) if any, then optional arguments + * (OARG_INT(default), etc) if any. The command will be given the default + * argument value if the user didn't give enough arguments to specify it. + * A command can take a maximum of MP_CMD_MAX_ARGS arguments (10). + */ + +#define ARG_INT { .type = {"", NULL, &m_option_type_int} } +#define ARG_FLOAT { .type = {"", NULL, &m_option_type_float} } +#define ARG_DOUBLE { .type = {"", NULL, &m_option_type_double} } +#define ARG_STRING { .type = {"", NULL, &m_option_type_string} } +#define ARG_CHOICE(c) { .type = {"", NULL, &m_option_type_choice, \ + M_CHOICES(c)} } +#define ARG_TIME { .type = {"", NULL, &m_option_type_time} } + +#define OARG_DOUBLE(def) { .type = {"", NULL, &m_option_type_double}, \ + .optional = true, .v.d = def } +#define OARG_INT(def) { .type = {"", NULL, &m_option_type_int}, \ + .optional = true, .v.i = def } +#define OARG_CHOICE(def, c) { .type = {"", NULL, &m_option_type_choice, \ + M_CHOICES(c)}, \ + .optional = true, .v.i = def } + +static int parse_cycle_dir(const struct m_option *opt, struct bstr name, + struct bstr param, void *dst); +static const struct m_option_type m_option_type_cycle_dir = { + .name = "up|down", + .parse = parse_cycle_dir, +}; + +static const mp_cmd_t mp_cmds[] = { + { MP_CMD_IGNORE, "ignore", }, + + { MP_CMD_RADIO_STEP_CHANNEL, "radio_step_channel", { ARG_INT } }, + { MP_CMD_RADIO_SET_CHANNEL, "radio_set_channel", { ARG_STRING } }, + { MP_CMD_RADIO_SET_FREQ, "radio_set_freq", { ARG_FLOAT } }, + { MP_CMD_RADIO_STEP_FREQ, "radio_step_freq", {ARG_FLOAT } }, + + { MP_CMD_SEEK, "seek", { + ARG_TIME, + OARG_CHOICE(0, ({"relative", 0}, {"0", 0}, + {"absolute-percent", 1}, {"1", 1}, + {"absolute", 2}, {"2", 2})), + OARG_CHOICE(0, ({"default-precise", 0}, {"0", 0}, + {"exact", 1}, {"1", 1}, + {"keyframes", -1}, {"-1", -1})), + }}, + { MP_CMD_SPEED_MULT, "speed_mult", { ARG_DOUBLE } }, + { MP_CMD_QUIT, "quit", { OARG_INT(0) } }, + { MP_CMD_QUIT_WATCH_LATER, "quit_watch_later", }, + { MP_CMD_STOP, "stop", }, + { MP_CMD_FRAME_STEP, "frame_step", }, + { MP_CMD_FRAME_BACK_STEP, "frame_back_step", }, + { MP_CMD_PLAYLIST_NEXT, "playlist_next", { + OARG_CHOICE(0, ({"weak", 0}, {"0", 0}, + {"force", 1}, {"1", 1})), + }}, + { MP_CMD_PLAYLIST_PREV, "playlist_prev", { + OARG_CHOICE(0, ({"weak", 0}, {"0", 0}, + {"force", 1}, {"1", 1})), + }}, + { MP_CMD_SUB_STEP, "sub_step", { ARG_INT } }, + { MP_CMD_OSD, "osd", { OARG_INT(-1) } }, + { MP_CMD_PRINT_TEXT, "print_text", { ARG_STRING } }, + { MP_CMD_SHOW_TEXT, "show_text", { ARG_STRING, OARG_INT(-1), OARG_INT(0) } }, + { MP_CMD_SHOW_PROGRESS, "show_progress", }, + { MP_CMD_SUB_ADD, "sub_add", { ARG_STRING } }, + { MP_CMD_SUB_REMOVE, "sub_remove", { OARG_INT(-1) } }, + { MP_CMD_SUB_RELOAD, "sub_reload", { OARG_INT(-1) } }, + + { MP_CMD_TV_START_SCAN, "tv_start_scan", }, + { MP_CMD_TV_STEP_CHANNEL, "tv_step_channel", { ARG_INT } }, + { MP_CMD_TV_STEP_NORM, "tv_step_norm", }, + { MP_CMD_TV_STEP_CHANNEL_LIST, "tv_step_chanlist", }, + { MP_CMD_TV_SET_CHANNEL, "tv_set_channel", { ARG_STRING } }, + { MP_CMD_TV_LAST_CHANNEL, "tv_last_channel", }, + { MP_CMD_TV_SET_FREQ, "tv_set_freq", { ARG_FLOAT } }, + { MP_CMD_TV_STEP_FREQ, "tv_step_freq", { ARG_FLOAT } }, + { MP_CMD_TV_SET_NORM, "tv_set_norm", { ARG_STRING } }, + + { MP_CMD_DVB_SET_CHANNEL, "dvb_set_channel", { ARG_INT, ARG_INT } }, + + { MP_CMD_SCREENSHOT, "screenshot", { + OARG_CHOICE(2, ({"video", 0}, + {"window", 1}, + {"subtitles", 2})), + OARG_CHOICE(0, ({"single", 0}, + {"each-frame", 1})), + }}, + { MP_CMD_SCREENSHOT_TO_FILE, "screenshot_to_file", { + ARG_STRING, + OARG_CHOICE(2, ({"video", 0}, + {"window", 1}, + {"subtitles", 2})), + }}, + { MP_CMD_LOADFILE, "loadfile", { + ARG_STRING, + OARG_CHOICE(0, ({"replace", 0}, {"0", 0}, + {"append", 1}, {"1", 1})), + }}, + { MP_CMD_LOADLIST, "loadlist", { + ARG_STRING, + OARG_CHOICE(0, ({"replace", 0}, {"0", 0}, + {"append", 1}, {"1", 1})), + }}, + { MP_CMD_PLAYLIST_CLEAR, "playlist_clear", }, + { MP_CMD_PLAYLIST_REMOVE, "playlist_remove", { ARG_INT } }, + { MP_CMD_PLAYLIST_MOVE, "playlist_move", { ARG_INT, ARG_INT } }, + { MP_CMD_RUN, "run", { ARG_STRING } }, + + { MP_CMD_KEYDOWN_EVENTS, "key_down_event", { ARG_INT } }, + { MP_CMD_SET, "set", { ARG_STRING, ARG_STRING } }, + { MP_CMD_GET_PROPERTY, "get_property", { ARG_STRING } }, + { MP_CMD_ADD, "add", { ARG_STRING, OARG_DOUBLE(0) } }, + { MP_CMD_CYCLE, "cycle", { + ARG_STRING, + { .type = {"", NULL, &m_option_type_cycle_dir}, + .optional = true, + .v.d = 1 }, + }}, + + { MP_CMD_ENABLE_INPUT_SECTION, "enable_section", { + ARG_STRING, + OARG_CHOICE(0, ({"default", 0}, + {"exclusive", 1})), + }}, + { MP_CMD_DISABLE_INPUT_SECTION, "disable_section", { ARG_STRING } }, + + { MP_CMD_AF, "af", { ARG_STRING, ARG_STRING } }, + + { MP_CMD_VF, "vf", { ARG_STRING, ARG_STRING } }, + + { MP_CMD_VO_CMDLINE, "vo_cmdline", { ARG_STRING } }, + + {0} +}; + +// Map legacy commands to proper commands +struct legacy_cmd { + const char *old, *new; +}; +static const struct legacy_cmd legacy_cmds[] = { + {"loop", "cycle loop"}, + {"seek_chapter", "add chapter"}, + {"switch_angle", "cycle angle"}, + {"pause", "cycle pause"}, + {"volume", "add volume"}, + {"mute", "cycle mute"}, + {"audio_delay", "add audio-delay"}, + {"switch_audio", "cycle audio"}, + {"balance", "add balance"}, + {"vo_fullscreen", "cycle fullscreen"}, + {"panscan", "add panscan"}, + {"vo_ontop", "cycle ontop"}, + {"vo_border", "cycle border"}, + {"frame_drop", "cycle framedrop"}, + {"gamma", "add gamma"}, + {"brightness", "add brightness"}, + {"contrast", "add contrast"}, + {"saturation", "add saturation"}, + {"hue", "add hue"}, + {"switch_vsync", "cycle vsync"}, + {"sub_load", "sub_add"}, + {"sub_select", "cycle sub"}, + {"sub_pos", "add sub-pos"}, + {"sub_delay", "add sub-delay"}, + {"sub_visibility", "cycle sub-visibility"}, + {"forced_subs_only", "cycle sub-forced-only"}, + {"sub_scale", "add sub-scale"}, + {"ass_use_margins", "cycle ass-use-margins"}, + {"tv_set_brightness", "add tv-brightness"}, + {"tv_set_hue", "add tv-hue"}, + {"tv_set_saturation", "add tv-saturation"}, + {"tv_set_contrast", "add tv-contrast"}, + {"step_property_osd", "cycle"}, + {"step_property", "no-osd cycle"}, + {"set_property", "no-osd set"}, + {"set_property_osd", "set"}, + {"speed_set", "set speed"}, + {"osd_show_text", "show_text"}, + {"osd_show_property_text", "show_text"}, + {"osd_show_progression", "show_progress"}, + {"show_chapters_osd", "show_text ${chapter-list}"}, + {"!show_chapters", "show_text ${chapter-list}"}, + {"show_tracks_osd", "show_text ${track-list}"}, + {"!show_tracks", "show_text ${track-list}"}, + {"!show_playlist", "show_text ${playlist}"}, + + // Approximate (can fail if user added additional whitespace) + {"pt_step 1", "playlist_next"}, + {"pt_step -1", "playlist_prev"}, + // Switch_ratio without argument resets aspect ratio + {"switch_ratio ", "set aspect "}, + {"switch_ratio", "set aspect 0"}, + {0} +}; + + +/// The names of the keys as used in input.conf +/// If you add some new keys, you also need to add them here + +static const struct key_name key_names[] = { + { ' ', "SPACE" }, + { '#', "SHARP" }, + { MP_KEY_ENTER, "ENTER" }, + { MP_KEY_TAB, "TAB" }, + { MP_KEY_BACKSPACE, "BS" }, + { MP_KEY_DELETE, "DEL" }, + { MP_KEY_INSERT, "INS" }, + { MP_KEY_HOME, "HOME" }, + { MP_KEY_END, "END" }, + { MP_KEY_PAGE_UP, "PGUP" }, + { MP_KEY_PAGE_DOWN, "PGDWN" }, + { MP_KEY_ESC, "ESC" }, + { MP_KEY_PRINT, "PRINT" }, + { MP_KEY_RIGHT, "RIGHT" }, + { MP_KEY_LEFT, "LEFT" }, + { MP_KEY_DOWN, "DOWN" }, + { MP_KEY_UP, "UP" }, + { MP_KEY_F+1, "F1" }, + { MP_KEY_F+2, "F2" }, + { MP_KEY_F+3, "F3" }, + { MP_KEY_F+4, "F4" }, + { MP_KEY_F+5, "F5" }, + { MP_KEY_F+6, "F6" }, + { MP_KEY_F+7, "F7" }, + { MP_KEY_F+8, "F8" }, + { MP_KEY_F+9, "F9" }, + { MP_KEY_F+10, "F10" }, + { MP_KEY_F+11, "F11" }, + { MP_KEY_F+12, "F12" }, + { MP_KEY_KP0, "KP0" }, + { MP_KEY_KP1, "KP1" }, + { MP_KEY_KP2, "KP2" }, + { MP_KEY_KP3, "KP3" }, + { MP_KEY_KP4, "KP4" }, + { MP_KEY_KP5, "KP5" }, + { MP_KEY_KP6, "KP6" }, + { MP_KEY_KP7, "KP7" }, + { MP_KEY_KP8, "KP8" }, + { MP_KEY_KP9, "KP9" }, + { MP_KEY_KPDEL, "KP_DEL" }, + { MP_KEY_KPDEC, "KP_DEC" }, + { MP_KEY_KPINS, "KP_INS" }, + { MP_KEY_KPENTER, "KP_ENTER" }, + { MP_MOUSE_BTN0, "MOUSE_BTN0" }, + { MP_MOUSE_BTN1, "MOUSE_BTN1" }, + { MP_MOUSE_BTN2, "MOUSE_BTN2" }, + { MP_MOUSE_BTN3, "MOUSE_BTN3" }, + { MP_MOUSE_BTN4, "MOUSE_BTN4" }, + { MP_MOUSE_BTN5, "MOUSE_BTN5" }, + { MP_MOUSE_BTN6, "MOUSE_BTN6" }, + { MP_MOUSE_BTN7, "MOUSE_BTN7" }, + { MP_MOUSE_BTN8, "MOUSE_BTN8" }, + { MP_MOUSE_BTN9, "MOUSE_BTN9" }, + { MP_MOUSE_BTN10, "MOUSE_BTN10" }, + { MP_MOUSE_BTN11, "MOUSE_BTN11" }, + { MP_MOUSE_BTN12, "MOUSE_BTN12" }, + { MP_MOUSE_BTN13, "MOUSE_BTN13" }, + { MP_MOUSE_BTN14, "MOUSE_BTN14" }, + { MP_MOUSE_BTN15, "MOUSE_BTN15" }, + { MP_MOUSE_BTN16, "MOUSE_BTN16" }, + { MP_MOUSE_BTN17, "MOUSE_BTN17" }, + { MP_MOUSE_BTN18, "MOUSE_BTN18" }, + { MP_MOUSE_BTN19, "MOUSE_BTN19" }, + { MP_MOUSE_BTN0_DBL, "MOUSE_BTN0_DBL" }, + { MP_MOUSE_BTN1_DBL, "MOUSE_BTN1_DBL" }, + { MP_MOUSE_BTN2_DBL, "MOUSE_BTN2_DBL" }, + { MP_MOUSE_BTN3_DBL, "MOUSE_BTN3_DBL" }, + { MP_MOUSE_BTN4_DBL, "MOUSE_BTN4_DBL" }, + { MP_MOUSE_BTN5_DBL, "MOUSE_BTN5_DBL" }, + { MP_MOUSE_BTN6_DBL, "MOUSE_BTN6_DBL" }, + { MP_MOUSE_BTN7_DBL, "MOUSE_BTN7_DBL" }, + { MP_MOUSE_BTN8_DBL, "MOUSE_BTN8_DBL" }, + { MP_MOUSE_BTN9_DBL, "MOUSE_BTN9_DBL" }, + { MP_MOUSE_BTN10_DBL, "MOUSE_BTN10_DBL" }, + { MP_MOUSE_BTN11_DBL, "MOUSE_BTN11_DBL" }, + { MP_MOUSE_BTN12_DBL, "MOUSE_BTN12_DBL" }, + { MP_MOUSE_BTN13_DBL, "MOUSE_BTN13_DBL" }, + { MP_MOUSE_BTN14_DBL, "MOUSE_BTN14_DBL" }, + { MP_MOUSE_BTN15_DBL, "MOUSE_BTN15_DBL" }, + { MP_MOUSE_BTN16_DBL, "MOUSE_BTN16_DBL" }, + { MP_MOUSE_BTN17_DBL, "MOUSE_BTN17_DBL" }, + { MP_MOUSE_BTN18_DBL, "MOUSE_BTN18_DBL" }, + { MP_MOUSE_BTN19_DBL, "MOUSE_BTN19_DBL" }, + { MP_JOY_AXIS1_MINUS, "JOY_UP" }, + { MP_JOY_AXIS1_PLUS, "JOY_DOWN" }, + { MP_JOY_AXIS0_MINUS, "JOY_LEFT" }, + { MP_JOY_AXIS0_PLUS, "JOY_RIGHT" }, + + { MP_JOY_AXIS0_PLUS, "JOY_AXIS0_PLUS" }, + { MP_JOY_AXIS0_MINUS, "JOY_AXIS0_MINUS" }, + { MP_JOY_AXIS1_PLUS, "JOY_AXIS1_PLUS" }, + { MP_JOY_AXIS1_MINUS, "JOY_AXIS1_MINUS" }, + { MP_JOY_AXIS2_PLUS, "JOY_AXIS2_PLUS" }, + { MP_JOY_AXIS2_MINUS, "JOY_AXIS2_MINUS" }, + { MP_JOY_AXIS3_PLUS, "JOY_AXIS3_PLUS" }, + { MP_JOY_AXIS3_MINUS, "JOY_AXIS3_MINUS" }, + { MP_JOY_AXIS4_PLUS, "JOY_AXIS4_PLUS" }, + { MP_JOY_AXIS4_MINUS, "JOY_AXIS4_MINUS" }, + { MP_JOY_AXIS5_PLUS, "JOY_AXIS5_PLUS" }, + { MP_JOY_AXIS5_MINUS, "JOY_AXIS5_MINUS" }, + { MP_JOY_AXIS6_PLUS, "JOY_AXIS6_PLUS" }, + { MP_JOY_AXIS6_MINUS, "JOY_AXIS6_MINUS" }, + { MP_JOY_AXIS7_PLUS, "JOY_AXIS7_PLUS" }, + { MP_JOY_AXIS7_MINUS, "JOY_AXIS7_MINUS" }, + { MP_JOY_AXIS8_PLUS, "JOY_AXIS8_PLUS" }, + { MP_JOY_AXIS8_MINUS, "JOY_AXIS8_MINUS" }, + { MP_JOY_AXIS9_PLUS, "JOY_AXIS9_PLUS" }, + { MP_JOY_AXIS9_MINUS, "JOY_AXIS9_MINUS" }, + + { MP_JOY_BTN0, "JOY_BTN0" }, + { MP_JOY_BTN1, "JOY_BTN1" }, + { MP_JOY_BTN2, "JOY_BTN2" }, + { MP_JOY_BTN3, "JOY_BTN3" }, + { MP_JOY_BTN4, "JOY_BTN4" }, + { MP_JOY_BTN5, "JOY_BTN5" }, + { MP_JOY_BTN6, "JOY_BTN6" }, + { MP_JOY_BTN7, "JOY_BTN7" }, + { MP_JOY_BTN8, "JOY_BTN8" }, + { MP_JOY_BTN9, "JOY_BTN9" }, + + { MP_AR_PLAY, "AR_PLAY" }, + { MP_AR_PLAY_HOLD, "AR_PLAY_HOLD" }, + { MP_AR_CENTER, "AR_CENTER" }, + { MP_AR_CENTER_HOLD, "AR_CENTER_HOLD" }, + { MP_AR_NEXT, "AR_NEXT" }, + { MP_AR_NEXT_HOLD, "AR_NEXT_HOLD" }, + { MP_AR_PREV, "AR_PREV" }, + { MP_AR_PREV_HOLD, "AR_PREV_HOLD" }, + { MP_AR_MENU, "AR_MENU" }, + { MP_AR_MENU_HOLD, "AR_MENU_HOLD" }, + { MP_AR_VUP, "AR_VUP" }, + { MP_AR_VUP_HOLD, "AR_VUP_HOLD" }, + { MP_AR_VDOWN, "AR_VDOWN" }, + { MP_AR_VDOWN_HOLD, "AR_VDOWN_HOLD" }, + + { MP_MK_PLAY, "MK_PLAY" }, + { MP_MK_PREV, "MK_PREV" }, + { MP_MK_NEXT, "MK_NEXT" }, + + { MP_KEY_POWER, "POWER" }, + { MP_KEY_MENU, "MENU" }, + { MP_KEY_PLAY, "PLAY" }, + { MP_KEY_PAUSE, "PAUSE" }, + { MP_KEY_PLAYPAUSE, "PLAYPAUSE" }, + { MP_KEY_STOP, "STOP" }, + { MP_KEY_FORWARD, "FORWARD" }, + { MP_KEY_REWIND, "REWIND" }, + { MP_KEY_NEXT, "NEXT" }, + { MP_KEY_PREV, "PREV" }, + { MP_KEY_VOLUME_UP, "VOLUME_UP" }, + { MP_KEY_VOLUME_DOWN, "VOLUME_DOWN" }, + { MP_KEY_MUTE, "MUTE" }, + + // These are kept for backward compatibility + { MP_KEY_PAUSE, "XF86_PAUSE" }, + { MP_KEY_STOP, "XF86_STOP" }, + { MP_KEY_PREV, "XF86_PREV" }, + { MP_KEY_NEXT, "XF86_NEXT" }, + + { MP_KEY_CLOSE_WIN, "CLOSE_WIN" }, + { MP_KEY_MOUSE_MOVE, "MOUSE_MOVE" }, + { MP_KEY_MOUSE_LEAVE, "MOUSE_LEAVE" }, + + { 0, NULL } +}; + +struct key_name modifier_names[] = { + { MP_KEY_MODIFIER_SHIFT, "Shift" }, + { MP_KEY_MODIFIER_CTRL, "Ctrl" }, + { MP_KEY_MODIFIER_ALT, "Alt" }, + { MP_KEY_MODIFIER_META, "Meta" }, + { 0 } +}; + +#define MP_MAX_FDS 10 + +struct input_fd { + int fd; + int (*read_key)(void *ctx, int fd); + int (*read_cmd)(int fd, char *dest, int size); + int (*close_func)(int fd); + void *ctx; + unsigned eof : 1; + unsigned drop : 1; + unsigned dead : 1; + unsigned got_cmd : 1; + unsigned select : 1; + // These fields are for the cmd fds. + char *buffer; + int pos, size; +}; + +struct cmd_bind_section { + struct cmd_bind *binds; + int num_binds; + char *section; + struct mp_rect mouse_area; // set at runtime, if at all + bool mouse_area_set; // mouse_area is valid and should be tested + struct cmd_bind_section *next; +}; + +#define MAX_ACTIVE_SECTIONS 5 + +struct active_section { + char *name; + int flags; +}; + +struct cmd_queue { + struct mp_cmd *first; +}; + +struct input_ctx { + bool using_ar; + bool using_cocoa_media_keys; + + // Autorepeat stuff + short ar_state; + int64_t last_ar; + // Autorepeat config + unsigned int ar_delay; + unsigned int ar_rate; + // Maximum number of queued commands from keypresses (limit to avoid + // repeated slow commands piling up) + int key_fifo_size; + + // these are the keys currently down + int key_down[MP_MAX_KEY_DOWN]; + unsigned int num_key_down; + int64_t last_key_down; + struct mp_cmd *current_down_cmd; + + int doubleclick_time; + int last_doubleclick_key_down; + double last_doubleclick_time; + + // Mouse position on the consumer side (as command.c sees it) + int mouse_x, mouse_y; + char *mouse_section; // last section to receive mouse event + + // Mouse position on the producer side (as the VO sees it) + // Unlike mouse_x/y, this can be used to resolve mouse click bindings. + int mouse_vo_x, mouse_vo_y; + + bool test; + + bool default_bindings; + // List of command binding sections + struct cmd_bind_section *cmd_bind_sections; + + // List currently active command sections + struct active_section active_sections[MAX_ACTIVE_SECTIONS]; + int num_active_sections; + + // Used to track whether we managed to read something while checking + // events sources. If yes, the sources may have more queued. + bool got_new_events; + + unsigned int mouse_event_counter; + + struct input_fd fds[MP_MAX_FDS]; + unsigned int num_fds; + + struct cmd_queue key_cmd_queue; + struct cmd_queue control_cmd_queue; + + int wakeup_pipe[2]; +}; + + +int async_quit_request; + +static int print_key_list(m_option_t *cfg, char *optname, char *optparam); +static int print_cmd_list(m_option_t *cfg, char *optname, char *optparam); + +#define OPT_BASE_STRUCT struct MPOpts + +// Our command line options +static const m_option_t input_config[] = { + OPT_STRING("conf", input.config_file, CONF_GLOBAL), + OPT_INT("ar-delay", input.ar_delay, CONF_GLOBAL), + OPT_INT("ar-rate", input.ar_rate, CONF_GLOBAL), + { "keylist", print_key_list, CONF_TYPE_PRINT_FUNC, CONF_GLOBAL | CONF_NOCFG }, + { "cmdlist", print_cmd_list, CONF_TYPE_PRINT_FUNC, CONF_GLOBAL | CONF_NOCFG }, + OPT_STRING("js-dev", input.js_dev, CONF_GLOBAL), + OPT_STRING("file", input.in_file, CONF_GLOBAL), + OPT_FLAG("default-bindings", input.default_bindings, CONF_GLOBAL), + OPT_FLAG("test", input.test, CONF_GLOBAL), + { NULL, NULL, 0, 0, 0, 0, NULL} +}; + +const m_option_t mp_input_opts[] = { + { "input", (void *)&input_config, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, + OPT_INTRANGE("doubleclick-time", input.doubleclick_time, 0, 0, 1000), + OPT_FLAG("joystick", input.use_joystick, CONF_GLOBAL), + OPT_FLAG("lirc", input.use_lirc, CONF_GLOBAL), + OPT_FLAG("lircc", input.use_lircc, CONF_GLOBAL), +#ifdef CONFIG_COCOA + OPT_FLAG("ar", input.use_ar, CONF_GLOBAL), + OPT_FLAG("media-keys", input.use_media_keys, CONF_GLOBAL), +#endif + { NULL, NULL, 0, 0, 0, 0, NULL} +}; + +static int default_cmd_func(int fd, char *buf, int l); + +static const char builtin_input_conf[] = +#include "core/input/input_conf.h" +; + +static bool test_rect(struct mp_rect *rc, int x, int y) +{ + return x >= rc->x0 && y >= rc->y0 && x < rc->x1 && y < rc->y1; +} + +static char *get_key_name(int key, char *ret) +{ + for (int i = 0; modifier_names[i].name; i++) { + if (modifier_names[i].key & key) { + ret = talloc_asprintf_append_buffer(ret, "%s+", + modifier_names[i].name); + key -= modifier_names[i].key; + } + } + for (int i = 0; key_names[i].name != NULL; i++) { + if (key_names[i].key == key) + return talloc_asprintf_append_buffer(ret, "%s", key_names[i].name); + } + + // printable, and valid unicode range + if (key >= 32 && key <= 0x10FFFF) + return mp_append_utf8_buffer(ret, key); + + // Print the hex key code + return talloc_asprintf_append_buffer(ret, "%#-8x", key); +} + +static char *get_key_combo_name(int *keys, int max) +{ + char *ret = talloc_strdup(NULL, ""); + while (max > 0) { + ret = get_key_name(*keys, ret); + if (--max && *++keys) + ret = talloc_asprintf_append_buffer(ret, "-"); + else + break; + } + return ret; +} + +bool mp_input_is_abort_cmd(int cmd_id) +{ + switch (cmd_id) { + case MP_CMD_QUIT: + case MP_CMD_PLAYLIST_NEXT: + case MP_CMD_PLAYLIST_PREV: + return true; + } + return false; +} + +static int queue_count_cmds(struct cmd_queue *queue) +{ + int res = 0; + for (struct mp_cmd *cmd = queue->first; cmd; cmd = cmd->queue_next) + res++; + return res; +} + +static bool queue_has_abort_cmds(struct cmd_queue *queue) +{ + for (struct mp_cmd *cmd = queue->first; cmd; cmd = cmd->queue_next) { + if (mp_input_is_abort_cmd(cmd->id)) + return true; + } + return false; +} + +static void queue_remove(struct cmd_queue *queue, struct mp_cmd *cmd) +{ + struct mp_cmd **p_prev = &queue->first; + while (*p_prev != cmd) { + p_prev = &(*p_prev)->queue_next; + } + // if this fails, cmd was not in the queue + assert(*p_prev == cmd); + *p_prev = cmd->queue_next; +} + +static void queue_add(struct cmd_queue *queue, struct mp_cmd *cmd, + bool at_head) +{ + if (at_head) { + cmd->queue_next = queue->first; + queue->first = cmd; + } else { + struct mp_cmd **p_prev = &queue->first; + while (*p_prev) + p_prev = &(*p_prev)->queue_next; + *p_prev = cmd; + cmd->queue_next = NULL; + } +} + +static struct input_fd *mp_input_add_fd(struct input_ctx *ictx) +{ + if (ictx->num_fds == MP_MAX_FDS) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Too many file descriptors.\n"); + return NULL; + } + + struct input_fd *fd = &ictx->fds[ictx->num_fds]; + *fd = (struct input_fd){ + .fd = -1, + }; + ictx->num_fds++; + + return fd; +} + +int mp_input_add_cmd_fd(struct input_ctx *ictx, int unix_fd, int select, + int read_func(int fd, char *dest, int size), + int close_func(int fd)) +{ + if (select && unix_fd < 0) { + mp_msg(MSGT_INPUT, MSGL_ERR, + "Invalid fd %d in mp_input_add_cmd_fd", unix_fd); + return 0; + } + + struct input_fd *fd = mp_input_add_fd(ictx); + if (!fd) + return 0; + fd->fd = unix_fd; + fd->select = select; + fd->read_cmd = read_func ? read_func : default_cmd_func; + fd->close_func = close_func; + return 1; +} + +int mp_input_add_key_fd(struct input_ctx *ictx, int unix_fd, int select, + int read_func(void *ctx, int fd), + int close_func(int fd), void *ctx) +{ + if (select && unix_fd < 0) { + mp_msg(MSGT_INPUT, MSGL_ERR, + "Invalid fd %d in mp_input_add_key_fd", unix_fd); + return 0; + } + assert(read_func); + + struct input_fd *fd = mp_input_add_fd(ictx); + if (!fd) + return 0; + fd->fd = unix_fd; + fd->select = select; + fd->read_key = read_func; + fd->close_func = close_func; + fd->ctx = ctx; + return 1; +} + + +static void mp_input_rm_fd(struct input_ctx *ictx, int fd) +{ + struct input_fd *fds = ictx->fds; + unsigned int i; + + for (i = 0; i < ictx->num_fds; i++) { + if (fds[i].fd == fd) + break; + } + if (i == ictx->num_fds) + return; + if (fds[i].close_func) + fds[i].close_func(fds[i].fd); + talloc_free(fds[i].buffer); + + if (i + 1 < ictx->num_fds) + memmove(&fds[i], &fds[i + 1], + (ictx->num_fds - i - 1) * sizeof(struct input_fd)); + ictx->num_fds--; +} + +void mp_input_rm_key_fd(struct input_ctx *ictx, int fd) +{ + mp_input_rm_fd(ictx, fd); +} + +static int parse_cycle_dir(const struct m_option *opt, struct bstr name, + struct bstr param, void *dst) +{ + double val; + if (bstrcmp0(param, "up") == 0) { + val = +1; + } else if (bstrcmp0(param, "down") == 0) { + val = -1; + } else { + return m_option_type_double.parse(opt, name, param, dst); + } + *(double *)dst = val; + return 1; +} + +static bool read_token(bstr str, bstr *out_rest, bstr *out_token) +{ + bstr t = bstr_lstrip(str); + int next = bstrcspn(t, WHITESPACE "#"); + // Handle comments + if (t.len && t.start[next] == '#') + t = bstr_splice(t, 0, next); + if (!t.len) + return false; + *out_token = bstr_splice(t, 0, next); + *out_rest = bstr_cut(t, next); + return true; +} + +static bool eat_token(bstr *str, const char *tok) +{ + bstr rest, token; + if (read_token(*str, &rest, &token) && bstrcmp0(token, tok) == 0) { + *str = rest; + return true; + } + return false; +} + +static bool read_escaped_string(void *talloc_ctx, bstr *str, bstr *literal) +{ + bstr t = *str; + char *new = talloc_strdup(talloc_ctx, ""); + while (t.len) { + if (t.start[0] == '"') + break; + if (t.start[0] == '\\') { + t = bstr_cut(t, 1); + if (!mp_parse_escape(&t, &new)) + goto error; + } else { + new = talloc_strndup_append_buffer(new, t.start, 1); + t = bstr_cut(t, 1); + } + } + int len = str->len - t.len; + *literal = new ? bstr0(new) : bstr_splice(*str, 0, len); + *str = bstr_cut(*str, len); + return true; +error: + talloc_free(new); + return false; +} + +// If dest is non-NULL when calling this function, append the command to the +// list formed by dest->queue_next, otherwise just set *dest = new_cmd; +static int parse_cmd(struct mp_cmd **dest, bstr str, const char *loc) +{ + int pausing = 0; + int on_osd = MP_ON_OSD_AUTO; + bool raw_args = false; + struct mp_cmd *cmd = NULL; + bstr start = str; + bstr next = {0}; + void *tmp = talloc_new(NULL); + + str = bstr_lstrip(str); + for (const struct legacy_cmd *entry = legacy_cmds; entry->old; entry++) { + bstr old = bstr0(entry->old); + bool silent = bstr_eatstart0(&old, "!"); + if (bstrcasecmp(bstr_splice(str, 0, old.len), old) == 0) { + if (!silent) { + mp_tmsg(MSGT_INPUT, MSGL_WARN, "Warning: command '%.*s' is " + "deprecated, replaced with '%s' at %s.\n", + BSTR_P(old), entry->new, loc); + } + bstr s = bstr_cut(str, old.len); + str = bstr0(talloc_asprintf(tmp, "%s%.*s", entry->new, BSTR_P(s))); + start = str; + break; + } + } + + while (1) { + if (eat_token(&str, "pausing")) { + pausing = 1; + } else if (eat_token(&str, "pausing_keep")) { + pausing = 2; + } else if (eat_token(&str, "pausing_toggle")) { + pausing = 3; + } else if (eat_token(&str, "pausing_keep_force")) { + pausing = 4; + } else if (eat_token(&str, "no-osd")) { + on_osd = MP_ON_OSD_NO; + } else if (eat_token(&str, "osd-bar")) { + on_osd = MP_ON_OSD_BAR; + } else if (eat_token(&str, "osd-msg")) { + on_osd = MP_ON_OSD_MSG; + } else if (eat_token(&str, "osd-msg-bar")) { + on_osd = MP_ON_OSD_MSG | MP_ON_OSD_BAR; + } else if (eat_token(&str, "osd-auto")) { + // default + } else if (eat_token(&str, "raw")) { + raw_args = true; + } else if (eat_token(&str, "expand-properties")) { + // default + } else { + break; + } + } + + int cmd_idx = 0; + while (mp_cmds[cmd_idx].name != NULL) { + if (eat_token(&str, mp_cmds[cmd_idx].name)) + break; + cmd_idx++; + } + + if (mp_cmds[cmd_idx].name == NULL) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command '%.*s' not found.\n", + BSTR_P(str)); + goto error; + } + + cmd = talloc_ptrtype(NULL, cmd); + *cmd = mp_cmds[cmd_idx]; + cmd->pausing = pausing; + cmd->on_osd = on_osd; + cmd->raw_args = raw_args; + + for (int i = 0; i < MP_CMD_MAX_ARGS; i++) { + struct mp_cmd_arg *cmdarg = &cmd->args[i]; + if (!cmdarg->type.type) + break; + str = bstr_lstrip(str); + if (eat_token(&str, ";")) { + next = str; + str.len = 0; + break; + } + cmd->nargs++; + bstr arg = {0}; + if (bstr_eatstart0(&str, "\"")) { + if (!read_escaped_string(tmp, &str, &arg)) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command %s: argument %d " + "has broken string escapes.\n", cmd->name, i + 1); + goto error; + } + if (!bstr_eatstart0(&str, "\"")) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command %s: argument %d is " + "unterminated.\n", cmd->name, i + 1); + goto error; + } + } else { + if (!read_token(str, &str, &arg)) + break; + if (cmdarg->optional && bstrcmp0(arg, "-") == 0) + continue; + } + // Prevent option API from trying to deallocate static strings + cmdarg->v = ((struct mp_cmd_arg) {{0}}).v; + int r = m_option_parse(&cmdarg->type, bstr0(cmd->name), arg, &cmdarg->v); + if (r < 0) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command %s: argument %d " + "can't be parsed: %s.\n", cmd->name, i + 1, + m_option_strerror(r)); + goto error; + } + if (cmdarg->type.type == &m_option_type_string) + cmdarg->v.s = talloc_steal(cmd, cmdarg->v.s); + } + + if (eat_token(&str, ";")) { + next = str; + str.len = 0; + } + + bstr dummy; + if (read_token(str, &dummy, &dummy)) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command %s has trailing unused " + "arguments: '%.*s'.\n", cmd->name, BSTR_P(str)); + // Better make it fatal to make it clear something is wrong. + goto error; + } + + int min_args = 0; + while (min_args < MP_CMD_MAX_ARGS && cmd->args[min_args].type.type + && !cmd->args[min_args].optional) + { + min_args++; + } + if (cmd->nargs < min_args) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command %s requires at least %d " + "arguments, we found only %d so far.\n", cmd->name, min_args, + cmd->nargs); + goto error; + } + + bstr orig = (bstr) {start.start, str.start - start.start}; + cmd->original = bstrdup(cmd, bstr_strip(orig)); + + while (*dest) + dest = &(*dest)->queue_next; + *dest = cmd; + + next = bstr_strip(next); + if (next.len) { + if (parse_cmd(dest, next, loc) < 0) { + *dest = NULL; + goto error; + } + } + + talloc_free(tmp); + return 1; + +error: + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command was defined at %s.\n", loc); + talloc_free(cmd); + talloc_free(tmp); + return -1; +} + +mp_cmd_t *mp_input_parse_cmd(bstr str, const char *loc) +{ + struct mp_cmd *cmd = NULL; + if (parse_cmd(&cmd, str, loc) < 0) { + assert(!cmd); + } + // Other input.c code uses queue_next for its own purposes, so explicitly + // wrap lists in a pseudo-command. + if (cmd && cmd->queue_next) { + struct mp_cmd *list = talloc_ptrtype(NULL, list); + *list = (struct mp_cmd) { + .id = MP_CMD_COMMAND_LIST, + .name = "list", + .original = bstrdup(list, str), + }; + list->args[0].v.p = cmd; + while (cmd) { + talloc_steal(list, cmd); + cmd = cmd->queue_next; + } + cmd = list; + } + return cmd; +} + +#define MP_CMD_MAX_SIZE 4096 + +static int read_cmd(struct input_fd *mp_fd, char **ret) +{ + char *end; + *ret = NULL; + + // Allocate the buffer if it doesn't exist + if (!mp_fd->buffer) { + mp_fd->buffer = talloc_size(NULL, MP_CMD_MAX_SIZE); + mp_fd->pos = 0; + mp_fd->size = MP_CMD_MAX_SIZE; + } + + // Get some data if needed/possible + while (!mp_fd->got_cmd && !mp_fd->eof && (mp_fd->size - mp_fd->pos > 1)) { + int r = mp_fd->read_cmd(mp_fd->fd, mp_fd->buffer + mp_fd->pos, + mp_fd->size - 1 - mp_fd->pos); + // Error ? + if (r < 0) { + switch (r) { + case MP_INPUT_ERROR: + case MP_INPUT_DEAD: + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Error while reading " + "command file descriptor %d: %s\n", + mp_fd->fd, strerror(errno)); + case MP_INPUT_NOTHING: + return r; + case MP_INPUT_RETRY: + continue; + } + // EOF ? + } else if (r == 0) { + mp_fd->eof = 1; + break; + } + mp_fd->pos += r; + break; + } + + mp_fd->got_cmd = 0; + + while (1) { + int l = 0; + // Find the cmd end + mp_fd->buffer[mp_fd->pos] = '\0'; + end = strchr(mp_fd->buffer, '\r'); + if (end) + *end = '\n'; + end = strchr(mp_fd->buffer, '\n'); + // No cmd end ? + if (!end) { + // If buffer is full we must drop all until the next \n + if (mp_fd->size - mp_fd->pos <= 1) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Command buffer of file " + "descriptor %d is full: dropping content.\n", + mp_fd->fd); + mp_fd->pos = 0; + mp_fd->drop = 1; + } + break; + } + // We already have a cmd : set the got_cmd flag + else if ((*ret)) { + mp_fd->got_cmd = 1; + break; + } + + l = end - mp_fd->buffer; + + // Not dropping : put the cmd in ret + if (!mp_fd->drop) + *ret = talloc_strndup(NULL, mp_fd->buffer, l); + else + mp_fd->drop = 0; + mp_fd->pos -= l + 1; + memmove(mp_fd->buffer, end + 1, mp_fd->pos); + } + + if (*ret) + return 1; + else + return MP_INPUT_NOTHING; +} + +static int default_cmd_func(int fd, char *buf, int l) +{ + while (1) { + int r = read(fd, buf, l); + // Error ? + if (r < 0) { + if (errno == EINTR) + continue; + else if (errno == EAGAIN) + return MP_INPUT_NOTHING; + return MP_INPUT_ERROR; + // EOF ? + } + return r; + } +} + +#ifndef __MINGW32__ +static int read_wakeup(void *ctx, int fd) +{ + char buf[100]; + read(fd, buf, sizeof(buf)); + return MP_INPUT_NOTHING; +} +#endif + +static bool bind_matches_key(struct cmd_bind *bind, int n, const int *keys); + +static void append_bind_info(char **pmsg, struct cmd_bind *bind) +{ + char *msg = *pmsg; + struct mp_cmd *cmd = mp_input_parse_cmd(bstr0(bind->cmd), bind->location); + bstr stripped = cmd ? cmd->original : bstr0(bind->cmd); + msg = talloc_asprintf_append(msg, " '%.*s'", BSTR_P(stripped)); + if (!cmd) + msg = talloc_asprintf_append(msg, " (invalid)"); + if (strcmp(bind->owner->section, "default") != 0) + msg = talloc_asprintf_append(msg, " in section {%s}", + bind->owner->section); + msg = talloc_asprintf_append(msg, " in %s", bind->location); + if (bind->is_builtin) + msg = talloc_asprintf_append(msg, " (default)"); + talloc_free(cmd); + *pmsg = msg; +} + +static mp_cmd_t *handle_test(struct input_ctx *ictx, int n, int *keys) +{ + char *key_buf = get_key_combo_name(keys, n); + // "$>" to disable property substitution when invoking "show_text" + char *msg = talloc_asprintf(NULL, "$>Key %s is bound to:\n", key_buf); + talloc_free(key_buf); + + int count = 0; + for (struct cmd_bind_section *bs = ictx->cmd_bind_sections; + bs; bs = bs->next) + { + for (int i = 0; i < bs->num_binds; i++) { + if (bind_matches_key(&bs->binds[i], n, keys)) { + count++; + msg = talloc_asprintf_append(msg, "%d. ", count); + append_bind_info(&msg, &bs->binds[i]); + msg = talloc_asprintf_append(msg, "\n"); + } + } + } + + if (!count) + msg = talloc_asprintf_append(msg, "(nothing)"); + + mp_msg(MSGT_INPUT, MSGL_V, "[input] %s\n", msg); + + mp_cmd_t *res = mp_input_parse_cmd(bstr0("show_text \"\""), ""); + res->args[0].v.s = talloc_steal(res, msg); + return res; +} + +static bool bind_matches_key(struct cmd_bind *bind, int num_keys, const int *keys) +{ + if (bind->num_keys != num_keys) + return false; + for (int i = 0; i < num_keys; i++) { + if (bind->keys[i] != keys[i]) + return false; + } + return true; +} + +static struct cmd_bind_section *get_bind_section(struct input_ctx *ictx, + bstr section) +{ + struct cmd_bind_section *bind_section = ictx->cmd_bind_sections; + + if (section.len == 0) + section = bstr0("default"); + while (bind_section) { + if (bstrcmp0(section, bind_section->section) == 0) + return bind_section; + if (bind_section->next == NULL) + break; + bind_section = bind_section->next; + } + if (bind_section) { + bind_section->next = talloc_ptrtype(ictx, bind_section->next); + bind_section = bind_section->next; + } else { + ictx->cmd_bind_sections = talloc_ptrtype(ictx, ictx->cmd_bind_sections); + bind_section = ictx->cmd_bind_sections; + } + *bind_section = (struct cmd_bind_section) { + .section = bstrdup0(bind_section, section), + }; + return bind_section; +} + +static struct cmd_bind *find_bind_for_key_section(struct input_ctx *ictx, + char *section, + int num_keys, int *keys) +{ + struct cmd_bind_section *bs = get_bind_section(ictx, bstr0(section)); + + if (!num_keys || !bs->num_binds) + return NULL; + + // Prefer user-defined keys over builtin bindings + for (int builtin = 0; builtin < 2; builtin++) { + for (int n = 0; n < bs->num_binds; n++) { + if (bs->binds[n].is_builtin == (bool)builtin && + bind_matches_key(&bs->binds[n], num_keys, keys)) + return &bs->binds[n]; + } + } + return NULL; +} + +static struct cmd_bind *find_any_bind_for_key(struct input_ctx *ictx, + char *force_section, + int n, int *keys) +{ + if (force_section) + return find_bind_for_key_section(ictx, force_section, n, keys); + + for (int i = ictx->num_active_sections - 1; i >= 0; i--) { + struct active_section *s = &ictx->active_sections[i]; + struct cmd_bind *bind = find_bind_for_key_section(ictx, s->name, n, keys); + if (bind) { + struct cmd_bind_section *bs = bind->owner; + for (int x = 0; x < n; x++) { + if (MP_KEY_DEPENDS_ON_MOUSE_POS(keys[x]) && bs->mouse_area_set && + !test_rect(&bs->mouse_area, ictx->mouse_vo_x, ictx->mouse_vo_y)) + goto skip; + } + return bind; + } + skip: ; + if (s->flags & MP_INPUT_EXCLUSIVE) + break; + } + + return NULL; +} + +static mp_cmd_t *get_cmd_from_keys(struct input_ctx *ictx, char *force_section, + int n, int *keys) +{ + if (ictx->test) + return handle_test(ictx, n, keys); + + struct cmd_bind *cmd = find_any_bind_for_key(ictx, force_section, n, keys); + if (cmd == NULL && n > 1) { + // Hitting two keys at once, and if there's no binding for this + // combination, the key hit last should be checked. + cmd = find_any_bind_for_key(ictx, force_section, 1, (int[]){keys[n - 1]}); + } + + if (cmd == NULL) { + char *key_buf = get_key_combo_name(keys, n); + mp_tmsg(MSGT_INPUT, MSGL_WARN, + "No bind found for key '%s'.\n", key_buf); + talloc_free(key_buf); + return NULL; + } + mp_cmd_t *ret = mp_input_parse_cmd(bstr0(cmd->cmd), cmd->location); + if (ret) { + ret->input_section = cmd->owner->section; + } else { + char *key_buf = get_key_combo_name(keys, n); + mp_tmsg(MSGT_INPUT, MSGL_ERR, + "Invalid command for bound key '%s': '%s'\n", key_buf, cmd->cmd); + talloc_free(key_buf); + } + return ret; +} + +static void release_down_cmd(struct input_ctx *ictx) +{ + if (ictx->current_down_cmd && ictx->current_down_cmd->key_up_follows) { + ictx->current_down_cmd->key_up_follows = false; + queue_add(&ictx->key_cmd_queue, ictx->current_down_cmd, false); + } else { + talloc_free(ictx->current_down_cmd); + } + ictx->current_down_cmd = NULL; + ictx->last_key_down = 0; + ictx->ar_state = -1; +} + +static int find_key_down(struct input_ctx *ictx, int code) +{ + code &= ~(MP_KEY_STATE_UP | MP_KEY_STATE_DOWN); + for (int j = 0; j < ictx->num_key_down; j++) { + if (ictx->key_down[j] == code) + return j; + } + return -1; +} + +static void remove_key_down(struct input_ctx *ictx, int code) +{ + int index = find_key_down(ictx, code); + if (index >= 0) { + memmove(&ictx->key_down[index], &ictx->key_down[index + 1], + (ictx->num_key_down - (index + 1)) * sizeof(int)); + ictx->num_key_down -= 1; + } +} + +static mp_cmd_t *interpret_key(struct input_ctx *ictx, int code) +{ + /* On normal keyboards shift changes the character code of non-special + * keys, so don't count the modifier separately for those. In other words + * we want to have "a" and "A" instead of "a" and "Shift+A"; but a separate + * shift modifier is still kept for special keys like arrow keys. + */ + int unmod = code & ~MP_KEY_MODIFIER_MASK; + if (unmod >= 32 && unmod < MP_KEY_BASE) + code &= ~MP_KEY_MODIFIER_SHIFT; + + if (!(code & MP_KEY_STATE_UP) && ictx->num_key_down >= MP_MAX_KEY_DOWN) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Too many key down events " + "at the same time\n"); + return NULL; + } + + bool key_was_down = find_key_down(ictx, code) >= 0; + + if (code & MP_KEY_STATE_DOWN) { + // Check if we don't already have this key as pushed + if (key_was_down) + return NULL; + // Cancel current down-event (there can be only one) + release_down_cmd(ictx); + ictx->key_down[ictx->num_key_down] = code & ~MP_KEY_STATE_DOWN; + ictx->num_key_down++; + ictx->last_key_down = mp_time_us(); + ictx->ar_state = 0; + ictx->current_down_cmd = get_cmd_from_keys(ictx, NULL, ictx->num_key_down, + ictx->key_down); + if (ictx->current_down_cmd && (code & MP_KEY_EMIT_ON_UP)) + ictx->current_down_cmd->key_up_follows = true; + return mp_cmd_clone(ictx->current_down_cmd); + } else if (code & MP_KEY_STATE_UP) { + if (key_was_down) { + remove_key_down(ictx, code); + release_down_cmd(ictx); + } + return NULL; + } else { + // Press of key with no separate down/up events + if (key_was_down) { + // Mixing press events and up/down with the same key is not allowed + mp_tmsg(MSGT_INPUT, MSGL_WARN, "Mixing key presses and up/down.\n"); + } + // Add temporarily (include ongoing down/up events) + int num_key_down = ictx->num_key_down; + int key_down[MP_MAX_KEY_DOWN]; + memcpy(key_down, ictx->key_down, num_key_down * sizeof(int)); + // Assume doubleclick events never use down/up, while button events do + if (MP_KEY_IS_MOUSE_BTN_DBL(code)) { + // Don't emit "MOUSE_BTN0+MOUSE_BTN0_DBL", just "MOUSE_BTN0_DBL" + int btn = code - MP_MOUSE_BTN0_DBL + MP_MOUSE_BTN0; + if (!num_key_down || key_down[num_key_down - 1] != btn) + return NULL; + key_down[num_key_down - 1] = code; + } else { + key_down[num_key_down] = code; + num_key_down++; + } + return get_cmd_from_keys(ictx, NULL, num_key_down, key_down); + } +} + +static mp_cmd_t *check_autorepeat(struct input_ctx *ictx) +{ + // No input : autorepeat ? + if (ictx->ar_rate > 0 && ictx->ar_state >= 0 && ictx->num_key_down > 0 + && !(ictx->key_down[ictx->num_key_down - 1] & MP_NO_REPEAT_KEY)) { + int64_t t = mp_time_us(); + if (ictx->last_ar + 2000000 < t) + ictx->last_ar = t; + // First time : wait delay + if (ictx->ar_state == 0 + && (t - ictx->last_key_down) >= ictx->ar_delay * 1000) + { + if (!ictx->current_down_cmd) { + ictx->ar_state = -1; + return NULL; + } + ictx->ar_state = 1; + ictx->last_ar = ictx->last_key_down + ictx->ar_delay * 1000; + return mp_cmd_clone(ictx->current_down_cmd); + // Then send rate / sec event + } else if (ictx->ar_state == 1 + && (t - ictx->last_ar) >= 1000000 / ictx->ar_rate) { + ictx->last_ar += 1000000 / ictx->ar_rate; + return mp_cmd_clone(ictx->current_down_cmd); + } + } + return NULL; +} + +static void add_key_cmd(struct input_ctx *ictx, struct mp_cmd *cmd) +{ + struct cmd_queue *queue = &ictx->key_cmd_queue; + if (queue_count_cmds(queue) >= ictx->key_fifo_size && + (!mp_input_is_abort_cmd(cmd->id) || queue_has_abort_cmds(queue))) + { + talloc_free(cmd); + return; + } + queue_add(queue, cmd, false); +} + +// Whether a command can deal with redundant key up events. +static bool key_updown_ok(enum mp_command_type cmd) +{ + switch (cmd) { + default: + return false; + } +} + +static void mp_input_feed_key(struct input_ctx *ictx, int code) +{ + ictx->got_new_events = true; + if (code == MP_INPUT_RELEASE_ALL) { + mp_msg(MSGT_INPUT, MSGL_V, "input: release all\n"); + ictx->num_key_down = 0; + release_down_cmd(ictx); + return; + } + int unmod = code & ~MP_KEY_MODIFIER_MASK; + if (MP_KEY_DEPENDS_ON_MOUSE_POS(unmod)) + ictx->mouse_event_counter++; + mp_msg(MSGT_INPUT, MSGL_V, "input: key code=%#x\n", code); + struct mp_cmd *cmd = interpret_key(ictx, code); + if (!cmd) + return; + // Prevent redundant key-down events from being added to the queue. In some + // cases (like MP_CMD_SEEK commands), duplicated events might severely + // confuse the frontend. + if (cmd->key_up_follows && !key_updown_ok(cmd->id)) { + talloc_free(cmd); + return; + } + add_key_cmd(ictx, cmd); +} + +void mp_input_put_key(struct input_ctx *ictx, int code) +{ + double now = mp_time_sec(); + int doubleclick_time = ictx->doubleclick_time; + // ignore system-doubleclick if we generate these events ourselves + int unmod = code & ~MP_KEY_MODIFIER_MASK; + if (doubleclick_time && MP_KEY_IS_MOUSE_BTN_DBL(unmod)) + return; + mp_input_feed_key(ictx, code); + if (code & MP_KEY_STATE_DOWN) { + code &= ~MP_KEY_STATE_DOWN; + if (ictx->last_doubleclick_key_down == code + && now - ictx->last_doubleclick_time < doubleclick_time / 1000.0) + { + if (code >= MP_MOUSE_BTN0 && code <= MP_MOUSE_BTN2) + mp_input_feed_key(ictx, code - MP_MOUSE_BTN0 + MP_MOUSE_BTN0_DBL); + } + ictx->last_doubleclick_key_down = code; + ictx->last_doubleclick_time = now; + } +} + +void mp_input_put_key_utf8(struct input_ctx *ictx, int mods, struct bstr t) +{ + while (t.len) { + int code = bstr_decode_utf8(t, &t); + if (code < 0) + break; + mp_input_put_key(ictx, code | mods); + } +} + +static void trigger_mouse_leave(struct input_ctx *ictx, char *new_section) +{ + if (!new_section) + new_section = "default"; + + char *old = ictx->mouse_section; + ictx->mouse_section = new_section; + + if (old && strcmp(old, ictx->mouse_section) != 0) { + struct mp_cmd *cmd = + get_cmd_from_keys(ictx, old, 1, (int[]){MP_KEY_MOUSE_LEAVE}); + if (cmd) + add_key_cmd(ictx, cmd); + } +} + + +void mp_input_set_mouse_pos(struct input_ctx *ictx, int x, int y) +{ + // we're already there + if (ictx->mouse_vo_x == x && ictx->mouse_vo_y == y) + return; + + ictx->mouse_event_counter++; + ictx->mouse_vo_x = x; + ictx->mouse_vo_y = y; + + struct mp_cmd *cmd = + get_cmd_from_keys(ictx, NULL, 1, (int[]){MP_KEY_MOUSE_MOVE}); + + trigger_mouse_leave(ictx, cmd ? cmd->input_section : NULL); + + if (!cmd) + return; + cmd->mouse_move = true; + cmd->mouse_x = x; + cmd->mouse_y = y; + add_key_cmd(ictx, cmd); +} + +static void read_cmd_fd(struct input_ctx *ictx, struct input_fd *cmd_fd) +{ + int r; + char *text; + while ((r = read_cmd(cmd_fd, &text)) >= 0) { + ictx->got_new_events = true; + struct mp_cmd *cmd = mp_input_parse_cmd(bstr0(text), "<pipe>"); + talloc_free(text); + if (cmd) + queue_add(&ictx->control_cmd_queue, cmd, false); + if (!cmd_fd->got_cmd) + return; + } + if (r == MP_INPUT_ERROR) + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Error on command file descriptor %d\n", + cmd_fd->fd); + else if (r == MP_INPUT_DEAD) + cmd_fd->dead = true; +} + +static void read_key_fd(struct input_ctx *ictx, struct input_fd *key_fd) +{ + int code = key_fd->read_key(key_fd->ctx, key_fd->fd); + if (code >= 0 || code == MP_INPUT_RELEASE_ALL) { + mp_input_feed_key(ictx, code); + return; + } + + if (code == MP_INPUT_ERROR) + mp_tmsg(MSGT_INPUT, MSGL_ERR, + "Error on key input file descriptor %d\n", key_fd->fd); + else if (code == MP_INPUT_DEAD) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, + "Dead key input on file descriptor %d\n", key_fd->fd); + key_fd->dead = true; + } +} + +static void read_fd(struct input_ctx *ictx, struct input_fd *fd) +{ + if (fd->read_cmd) { + read_cmd_fd(ictx, fd); + } else { + read_key_fd(ictx, fd); + } +} + +static void remove_dead_fds(struct input_ctx *ictx) +{ + for (int i = 0; i < ictx->num_fds; i++) { + if (ictx->fds[i].dead) { + mp_input_rm_fd(ictx, ictx->fds[i].fd); + i--; + } + } +} + +#ifdef HAVE_POSIX_SELECT + +static void input_wait_read(struct input_ctx *ictx, int time) +{ + fd_set fds; + FD_ZERO(&fds); + int max_fd = 0; + for (int i = 0; i < ictx->num_fds; i++) { + if (!ictx->fds[i].select) + continue; + if (ictx->fds[i].fd > max_fd) + max_fd = ictx->fds[i].fd; + FD_SET(ictx->fds[i].fd, &fds); + } + struct timeval tv, *time_val; + tv.tv_sec = time / 1000; + tv.tv_usec = (time % 1000) * 1000; + time_val = &tv; + if (select(max_fd + 1, &fds, NULL, NULL, time_val) < 0) { + if (errno != EINTR) + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Select error: %s\n", + strerror(errno)); + FD_ZERO(&fds); + } + for (int i = 0; i < ictx->num_fds; i++) { + if (ictx->fds[i].select && !FD_ISSET(ictx->fds[i].fd, &fds)) + continue; + read_fd(ictx, &ictx->fds[i]); + } +} + +#else + +static void input_wait_read(struct input_ctx *ictx, int time) +{ + if (time > 0) + mp_sleep_us(time * 1000); + + for (int i = 0; i < ictx->num_fds; i++) + read_fd(ictx, &ictx->fds[i]); +} + +#endif + +/** + * \param time time to wait at most for an event in milliseconds + */ +static void read_events(struct input_ctx *ictx, int time) +{ + if (ictx->num_key_down) { + time = FFMIN(time, 1000 / ictx->ar_rate); + time = FFMIN(time, ictx->ar_delay); + } + time = FFMAX(time, 0); + + while (1) { + if (ictx->got_new_events) + time = 0; + ictx->got_new_events = false; + + remove_dead_fds(ictx); + + if (time) { + for (int i = 0; i < ictx->num_fds; i++) { + if (!ictx->fds[i].select) + read_fd(ictx, &ictx->fds[i]); + } + } + + if (ictx->got_new_events) + time = 0; + + input_wait_read(ictx, time); + + // Read until all input FDs are empty + if (!ictx->got_new_events) + break; + } +} + +static void read_all_events(struct input_ctx *ictx, int time) +{ + getch2_poll(); +#ifdef CONFIG_COCOA + cocoa_check_events(); +#endif + read_events(ictx, time); +} + +int mp_input_queue_cmd(struct input_ctx *ictx, mp_cmd_t *cmd) +{ + ictx->got_new_events = true; + if (!cmd) + return 0; + queue_add(&ictx->control_cmd_queue, cmd, false); + return 1; +} + +/** + * \param peek_only when set, the returned command stays in the queue. + * Do not free the returned cmd whe you set this! + */ +mp_cmd_t *mp_input_get_cmd(struct input_ctx *ictx, int time, int peek_only) +{ + if (async_quit_request) { + struct mp_cmd *cmd = mp_input_parse_cmd(bstr0("quit 1"), ""); + queue_add(&ictx->control_cmd_queue, cmd, true); + } + + if (ictx->control_cmd_queue.first || ictx->key_cmd_queue.first) + time = 0; + read_all_events(ictx, time); + struct cmd_queue *queue = &ictx->control_cmd_queue; + if (!queue->first) + queue = &ictx->key_cmd_queue; + if (!queue->first) { + struct mp_cmd *repeated = check_autorepeat(ictx); + if (repeated) + queue_add(queue, repeated, false); + } + struct mp_cmd *ret = queue->first; + if (!ret) + return NULL; + + if (!peek_only) { + queue_remove(queue, ret); + if (ret->mouse_move) { + ictx->mouse_x = ret->mouse_x; + ictx->mouse_y = ret->mouse_y; + } + } + + return ret; +} + +void mp_input_get_mouse_pos(struct input_ctx *ictx, int *x, int *y) +{ + *x = ictx->mouse_x; + *y = ictx->mouse_y; +} + +void mp_cmd_free(mp_cmd_t *cmd) +{ + talloc_free(cmd); +} + +mp_cmd_t *mp_cmd_clone(mp_cmd_t *cmd) +{ + mp_cmd_t *ret; + int i; + + if (!cmd) + return NULL; + + ret = talloc_memdup(NULL, cmd, sizeof(mp_cmd_t)); + ret->name = talloc_strdup(ret, cmd->name); + for (i = 0; i < MP_CMD_MAX_ARGS; i++) { + if (cmd->args[i].type.type == &m_option_type_string) + ret->args[i].v.s = talloc_strdup(ret, cmd->args[i].v.s); + } + + if (cmd->id == MP_CMD_COMMAND_LIST) { + struct mp_cmd *prev = NULL; + for (struct mp_cmd *sub = cmd->args[0].v.p; sub; sub = sub->queue_next) { + sub = mp_cmd_clone(sub); + talloc_steal(ret, sub); + if (prev) { + prev->queue_next = sub; + } else { + ret->args[0].v.p = sub; + } + prev = sub; + } + } + + return ret; +} + +int mp_input_get_key_from_name(const char *name) +{ + int modifiers = 0; + const char *p; + while ((p = strchr(name, '+'))) { + for (struct key_name *m = modifier_names; m->name; m++) + if (!bstrcasecmp(bstr0(m->name), + (struct bstr){(char *)name, p - name})) { + modifiers |= m->key; + goto found; + } + if (!strcmp(name, "+")) + return '+' + modifiers; + return -1; +found: + name = p + 1; + } + + struct bstr bname = bstr0(name); + + struct bstr rest; + int code = bstr_decode_utf8(bname, &rest); + if (code >= 0 && rest.len == 0) + return code + modifiers; + + if (bstr_startswith0(bname, "0x")) + return strtol(name, NULL, 16) + modifiers; + + for (int i = 0; key_names[i].name != NULL; i++) { + if (strcasecmp(key_names[i].name, name) == 0) + return key_names[i].key + modifiers; + } + + return -1; +} + +static int get_input_from_name(char *name, int *out_num_keys, int *keys) +{ + char *end, *ptr; + int n = 0; + + ptr = name; + n = 0; + for (end = strchr(ptr, '-'); ptr != NULL; end = strchr(ptr, '-')) { + if (end && end[1] != '\0') { + if (end[1] == '-') + end = &end[1]; + end[0] = '\0'; + } + keys[n] = mp_input_get_key_from_name(ptr); + if (keys[n] < 0) + return 0; + n++; + if (end && end[1] != '\0' && n < MP_MAX_KEY_DOWN) + ptr = &end[1]; + else + break; + } + *out_num_keys = n; + return 1; +} + +static void bind_dealloc(struct cmd_bind *bind) +{ + talloc_free(bind->cmd); + talloc_free(bind->location); +} + +static void bind_keys(struct input_ctx *ictx, bool builtin, bstr section, + const int *keys, int num_keys, bstr command, + const char *loc) +{ + struct cmd_bind_section *bs = get_bind_section(ictx, section); + struct cmd_bind *bind = NULL; + + assert(num_keys <= MP_MAX_KEY_DOWN); + + for (int n = 0; n < bs->num_binds; n++) { + struct cmd_bind *b = &bs->binds[n]; + if (bind_matches_key(b, num_keys, keys) && b->is_builtin == builtin) { + bind = b; + break; + } + } + + if (!bind) { + struct cmd_bind empty = {{0}}; + MP_TARRAY_APPEND(bs, bs->binds, bs->num_binds, empty); + bind = &bs->binds[bs->num_binds - 1]; + } + + bind_dealloc(bind); + + *bind = (struct cmd_bind) { + .cmd = bstrdup0(bs->binds, command), + .location = talloc_strdup(bs->binds, loc), + .owner = bs, + .is_builtin = builtin, + .num_keys = num_keys, + }; + memcpy(bind->keys, keys, num_keys * sizeof(bind->keys[0])); +} + +// restrict_section: every entry is forced to this section name +// if NULL, load normally and allow any sections +static int parse_config(struct input_ctx *ictx, bool builtin, bstr data, + const char *location, const char *restrict_section) +{ + int n_binds = 0; + int line_no = 0; + char *cur_loc = NULL; + + while (data.len) { + line_no++; + if (cur_loc) + talloc_free(cur_loc); + cur_loc = talloc_asprintf(NULL, "%s:%d", location, line_no); + + bstr line = bstr_strip_linebreaks(bstr_getline(data, &data)); + line = bstr_lstrip(line); + if (line.len == 0 || bstr_startswith0(line, "#")) + continue; + struct bstr command; + // Find the key name starting a line + struct bstr keyname = bstr_split(line, WHITESPACE, &command); + command = bstr_strip(command); + if (command.len == 0) { + mp_tmsg(MSGT_INPUT, MSGL_ERR, + "Unfinished key binding: %.*s at %s\n", BSTR_P(line), + cur_loc); + continue; + } + char *name = bstrdup0(NULL, keyname); + int keys[MP_MAX_KEY_DOWN]; + int num_keys = 0; + if (!get_input_from_name(name, &num_keys, keys)) { + talloc_free(name); + mp_tmsg(MSGT_INPUT, MSGL_ERR, + "Unknown key '%.*s' at %s\n", BSTR_P(keyname), cur_loc); + continue; + } + talloc_free(name); + + bstr section = bstr0(restrict_section); + if (!section.len) { + if (bstr_startswith0(command, "{")) { + int p = bstrchr(command, '}'); + if (p != -1) { + section = bstr_strip(bstr_splice(command, 1, p)); + command = bstr_lstrip(bstr_cut(command, p + 1)); + } + } + } + + bind_keys(ictx, builtin, section, keys, num_keys, command, cur_loc); + n_binds++; + + // Print warnings if invalid commands are encountered. + talloc_free(mp_input_parse_cmd(command, cur_loc)); + } + + talloc_free(cur_loc); + + return n_binds; +} + +static int parse_config_file(struct input_ctx *ictx, char *file, bool warn) +{ + if (!mp_path_exists(file)) { + mp_msg(MSGT_INPUT, warn ? MSGL_ERR : MSGL_V, + "Input config file %s not found.\n", file); + return 0; + } + stream_t *s = stream_open(file, NULL); + if (!s) { + mp_msg(MSGT_INPUT, MSGL_ERR, "Can't open input config file %s.\n", file); + return 0; + } + bstr res = stream_read_complete(s, NULL, 1000000); + free_stream(s); + mp_msg(MSGT_INPUT, MSGL_V, "Parsing input config file %s\n", file); + int n_binds = parse_config(ictx, false, res, file, NULL); + talloc_free(res.start); + mp_msg(MSGT_INPUT, MSGL_V, "Input config file %s parsed: %d binds\n", + file, n_binds); + return 1; +} + +// If name is NULL, return "default". +// Return a statically allocated name of the section (i.e. return value never +// gets deallocated). +static char *normalize_section(struct input_ctx *ictx, char *name) +{ + return get_bind_section(ictx, bstr0(name))->section; +} + +void mp_input_disable_section(struct input_ctx *ictx, char *name) +{ + name = normalize_section(ictx, name); + + // Remove old section, or make sure it's on top if re-enabled + for (int i = ictx->num_active_sections - 1; i >= 0; i--) { + struct active_section *as = &ictx->active_sections[i]; + if (strcmp(as->name, name) == 0) { + for (int x = i; i < ictx->num_active_sections - 1; i++) + ictx->active_sections[x] = ictx->active_sections[x + 1]; + ictx->num_active_sections--; + } + } +} + +void mp_input_enable_section(struct input_ctx *ictx, char *name, int flags) +{ + name = normalize_section(ictx, name); + + mp_input_disable_section(ictx, name); + + if (ictx->num_active_sections < MAX_ACTIVE_SECTIONS) { + ictx->active_sections[ictx->num_active_sections++] = + (struct active_section) {name, flags}; + } +} + +void mp_input_disable_all_sections(struct input_ctx *ictx) +{ + ictx->num_active_sections = 0; +} + +void mp_input_set_section_mouse_area(struct input_ctx *ictx, char *name, + int x0, int y0, int x1, int y1) +{ + struct cmd_bind_section *s = get_bind_section(ictx, bstr0(name)); + s->mouse_area = (struct mp_rect){x0, y0, x1, y1}; + s->mouse_area_set = x0 != x1 && y0 != y1; +} + +bool mp_input_test_mouse_active(struct input_ctx *ictx, int x, int y) +{ + for (int i = 0; i < ictx->num_active_sections; i++) { + char *name = ictx->active_sections[i].name; + struct cmd_bind_section *s = get_bind_section(ictx, bstr0(name)); + if (s->mouse_area_set && test_rect(&s->mouse_area, x, y)) + return true; + } + return false; +} + +bool mp_input_test_dragging(struct input_ctx *ictx, int x, int y) +{ + return mp_input_test_mouse_active(ictx, x, y); +} + +// builtin: if true, remove all builtin binds, else remove all user binds +static void remove_binds(struct cmd_bind_section *bs, bool builtin) +{ + for (int n = bs->num_binds - 1; n >= 0; n--) { + if (bs->binds[n].is_builtin == builtin) { + bind_dealloc(&bs->binds[n]); + assert(bs->num_binds >= 1); + bs->binds[n] = bs->binds[bs->num_binds - 1]; + bs->num_binds--; + } + } +} + +void mp_input_define_section(struct input_ctx *ictx, char *name, char *location, + char *contents, bool builtin) +{ + if (!name || !name[0]) + return; // parse_config() changes semantics with restrict_section==empty + if (contents) { + parse_config(ictx, builtin, bstr0(contents), location, name); + } else { + // Disable: + mp_input_disable_section(ictx, name); + // Delete: + struct cmd_bind_section *bs = get_bind_section(ictx, bstr0(name)); + remove_binds(bs, builtin); + } +} + +struct input_ctx *mp_input_init(struct MPOpts *opts) +{ + struct input_conf *input_conf = &opts->input; + + struct input_ctx *ictx = talloc_ptrtype(NULL, ictx); + *ictx = (struct input_ctx){ + .key_fifo_size = input_conf->key_fifo_size, + .doubleclick_time = input_conf->doubleclick_time, + .ar_state = -1, + .ar_delay = input_conf->ar_delay, + .ar_rate = input_conf->ar_rate, + .default_bindings = input_conf->default_bindings, + .test = input_conf->test, + .wakeup_pipe = {-1, -1}, + }; + mp_input_enable_section(ictx, NULL, 0); + + parse_config(ictx, true, bstr0(builtin_input_conf), "<builtin>", NULL); + +#ifndef __MINGW32__ + long ret = pipe(ictx->wakeup_pipe); + for (int i = 0; i < 2 && ret >= 0; i++) { + ret = fcntl(ictx->wakeup_pipe[i], F_GETFL); + if (ret < 0) + break; + ret = fcntl(ictx->wakeup_pipe[i], F_SETFL, ret | O_NONBLOCK); + } + if (ret < 0) + mp_msg(MSGT_INPUT, MSGL_ERR, + "Failed to initialize wakeup pipe: %s\n", strerror(errno)); + else + mp_input_add_key_fd(ictx, ictx->wakeup_pipe[0], true, read_wakeup, + NULL, NULL); +#endif + + bool config_ok = false; + if (input_conf->config_file) + config_ok = parse_config_file(ictx, input_conf->config_file, true); + if (!config_ok && opts->load_config) { + // Try global conf dir + char *file = mp_find_config_file("input.conf"); + config_ok = file && parse_config_file(ictx, file, false); + talloc_free(file); + } + if (!config_ok) { + mp_msg(MSGT_INPUT, MSGL_V, "Falling back on default (hardcoded) " + "input config\n"); + } + +#ifdef CONFIG_JOYSTICK + if (input_conf->use_joystick) { + int fd = mp_input_joystick_init(input_conf->js_dev); + if (fd < 0) + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Can't init input joystick\n"); + else + mp_input_add_key_fd(ictx, fd, 1, mp_input_joystick_read, + close, NULL); + } +#endif + +#ifdef CONFIG_LIRC + if (input_conf->use_lirc) { + int fd = mp_input_lirc_init(); + if (fd > 0) + mp_input_add_cmd_fd(ictx, fd, 0, mp_input_lirc_read, + mp_input_lirc_close); + } +#endif + +#ifdef CONFIG_LIRCC + if (input_conf->use_lircc) { + int fd = lircc_init("mpv", NULL); + if (fd >= 0) + mp_input_add_cmd_fd(ictx, fd, 1, NULL, lircc_cleanup); + } +#endif + +#ifdef CONFIG_COCOA + if (input_conf->use_ar) { + cocoa_init_apple_remote(); + ictx->using_ar = true; + } + + if (input_conf->use_media_keys) { + cocoa_init_media_keys(); + ictx->using_cocoa_media_keys = true; + } +#endif + + if (input_conf->in_file) { + int mode = O_RDONLY; +#ifndef __MINGW32__ + // Use RDWR for FIFOs to ensure they stay open over multiple accesses. + // Note that on Windows due to how the API works, using RDONLY should + // be ok. + struct stat st; + if (stat(input_conf->in_file, &st) == 0 && S_ISFIFO(st.st_mode)) + mode = O_RDWR; + mode |= O_NONBLOCK; +#endif + int in_file_fd = open(input_conf->in_file, mode); + if (in_file_fd >= 0) + mp_input_add_cmd_fd(ictx, in_file_fd, 1, NULL, close); + else + mp_tmsg(MSGT_INPUT, MSGL_ERR, "Can't open %s: %s\n", + input_conf->in_file, strerror(errno)); + } + + return ictx; +} + +static void clear_queue(struct cmd_queue *queue) +{ + while (queue->first) { + struct mp_cmd *item = queue->first; + queue_remove(queue, item); + talloc_free(item); + } +} + +void mp_input_uninit(struct input_ctx *ictx) +{ + if (!ictx) + return; + +#ifdef CONFIG_COCOA + if (ictx->using_ar) { + cocoa_uninit_apple_remote(); + } + + if (ictx->using_cocoa_media_keys) { + cocoa_uninit_media_keys(); + } +#endif + + for (int i = 0; i < ictx->num_fds; i++) { + if (ictx->fds[i].close_func) + ictx->fds[i].close_func(ictx->fds[i].fd); + } + for (int i = 0; i < 2; i++) { + if (ictx->wakeup_pipe[i] != -1) + close(ictx->wakeup_pipe[i]); + } + clear_queue(&ictx->key_cmd_queue); + clear_queue(&ictx->control_cmd_queue); + talloc_free(ictx->current_down_cmd); + talloc_free(ictx); +} + +static int print_key_list(m_option_t *cfg, char *optname, char *optparam) +{ + int i; + printf("\n"); + for (i = 0; key_names[i].name != NULL; i++) + printf("%s\n", key_names[i].name); + return M_OPT_EXIT; +} + +static int print_cmd_list(m_option_t *cfg, char *optname, char *optparam) +{ + const mp_cmd_t *cmd; + int i, j; + + for (i = 0; (cmd = &mp_cmds[i])->name != NULL; i++) { + printf("%-20.20s", cmd->name); + for (j = 0; j < MP_CMD_MAX_ARGS && cmd->args[j].type.type; j++) { + const char *type = cmd->args[j].type.type->name; + if (cmd->args[j].optional) + printf(" [%s]", type); + else + printf(" %s", type); + } + printf("\n"); + } + return M_OPT_EXIT; +} + +void mp_input_wakeup(struct input_ctx *ictx) +{ + if (ictx->wakeup_pipe[1] >= 0) + write(ictx->wakeup_pipe[1], &(char){0}, 1); +} + +/** + * \param time time to wait for an interruption in milliseconds + */ +int mp_input_check_interrupt(struct input_ctx *ictx, int time) +{ + for (int i = 0; ; i++) { + if (async_quit_request || queue_has_abort_cmds(&ictx->key_cmd_queue) || + queue_has_abort_cmds(&ictx->control_cmd_queue)) { + mp_tmsg(MSGT_INPUT, MSGL_WARN, "Received command to move to " + "another file. Aborting current processing.\n"); + return true; + } + if (i) + return false; + read_all_events(ictx, time); + } +} + +unsigned int mp_input_get_mouse_event_counter(struct input_ctx *ictx) +{ + // Make the frontend always display the mouse cursor (as long as it's not + // forced invisible) if mouse input is desired. + if (mp_input_test_mouse_active(ictx, ictx->mouse_x, ictx->mouse_y)) + ictx->mouse_event_counter++; + return ictx->mouse_event_counter; +} diff --git a/mpvcore/input/input.h b/mpvcore/input/input.h new file mode 100644 index 0000000000..92e2a32c4f --- /dev/null +++ b/mpvcore/input/input.h @@ -0,0 +1,273 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_INPUT_H +#define MPLAYER_INPUT_H + +#include <stdbool.h> +#include "core/bstr.h" +#include "core/m_option.h" + +// All command IDs +enum mp_command_type { + MP_CMD_IGNORE, + MP_CMD_SEEK, + MP_CMD_QUIT, + MP_CMD_QUIT_WATCH_LATER, + MP_CMD_PLAYLIST_NEXT, + MP_CMD_PLAYLIST_PREV, + MP_CMD_OSD, + MP_CMD_TV_STEP_CHANNEL, + MP_CMD_TV_STEP_NORM, + MP_CMD_TV_STEP_CHANNEL_LIST, + MP_CMD_SCREENSHOT, + MP_CMD_SCREENSHOT_TO_FILE, + MP_CMD_LOADFILE, + MP_CMD_LOADLIST, + MP_CMD_PLAYLIST_CLEAR, + MP_CMD_PLAYLIST_REMOVE, + MP_CMD_PLAYLIST_MOVE, + MP_CMD_SUB_STEP, + MP_CMD_TV_SET_CHANNEL, + MP_CMD_TV_LAST_CHANNEL, + MP_CMD_TV_SET_FREQ, + MP_CMD_TV_SET_NORM, + MP_CMD_FRAME_STEP, + MP_CMD_FRAME_BACK_STEP, + MP_CMD_SPEED_MULT, + MP_CMD_RUN, + MP_CMD_SUB_ADD, + MP_CMD_SUB_REMOVE, + MP_CMD_SUB_RELOAD, + MP_CMD_KEYDOWN_EVENTS, + MP_CMD_SET, + MP_CMD_GET_PROPERTY, + MP_CMD_PRINT_TEXT, + MP_CMD_SHOW_TEXT, + MP_CMD_SHOW_PROGRESS, + MP_CMD_RADIO_STEP_CHANNEL, + MP_CMD_RADIO_SET_CHANNEL, + MP_CMD_RADIO_SET_FREQ, + MP_CMD_ADD, + MP_CMD_CYCLE, + MP_CMD_RADIO_STEP_FREQ, + MP_CMD_TV_STEP_FREQ, + MP_CMD_TV_START_SCAN, + MP_CMD_STOP, + + MP_CMD_ENABLE_INPUT_SECTION, + MP_CMD_DISABLE_INPUT_SECTION, + + /// DVB commands + MP_CMD_DVB_SET_CHANNEL = 5101, + + /// Audio Filter commands + MP_CMD_AF, + + /// Video filter commands + MP_CMD_VF, + + /// Video output commands + MP_CMD_VO_CMDLINE, + + // Internal + MP_CMD_COMMAND_LIST, // list of sub-commands in args[0].v.p +}; + +#define MP_CMD_MAX_ARGS 10 + +// Error codes for the drivers + +// An error occurred but we can continue +#define MP_INPUT_ERROR -1 +// A fatal error occurred, this driver should be removed +#define MP_INPUT_DEAD -2 +// No input was available +#define MP_INPUT_NOTHING -3 +//! Input will be available if you try again +#define MP_INPUT_RETRY -4 +// Key FIFO was full - release events may be lost, zero button-down status +#define MP_INPUT_RELEASE_ALL -5 + +enum mp_on_osd { + MP_ON_OSD_NO = 0, // prefer not using OSD + MP_ON_OSD_AUTO = 1, // use default behavior of the specific command + MP_ON_OSD_BAR = 2, // force a bar, if applicable + MP_ON_OSD_MSG = 4, // force a message, if applicable +}; + +enum mp_input_section_flags { + // If a key binding is not defined in the current section, do not search the + // other sections for it (like the default section). Instead, an unbound + // key warning will be printed. + MP_INPUT_EXCLUSIVE = 1, +}; + +struct input_ctx; + +struct mp_cmd_arg { + struct m_option type; + bool optional; + union { + int i; + float f; + double d; + char *s; + void *p; + } v; +}; + +typedef struct mp_cmd { + int id; + char *name; + struct mp_cmd_arg args[MP_CMD_MAX_ARGS]; + int nargs; + int pausing; + bool raw_args; + enum mp_on_osd on_osd; + bstr original; + char *input_section; + bool key_up_follows; + bool mouse_move; + int mouse_x, mouse_y; + struct mp_cmd *queue_next; +} mp_cmd_t; + + +// Executing this command will abort playback (play something else, or quit). +bool mp_input_is_abort_cmd(int cmd_id); + +/* Add a new command input source. + * "fd" is a file descriptor (use a negative value if you don't use any fd) + * "select" tells whether to use select() on the fd to determine when to + * try reading. + * "read_func" is optional. If NULL a default function which reads data + * directly from the fd will be used. It must return either text data + * or one of the MP_INPUT error codes above. + * "close_func" will be called when closing. Can be NULL. Its return value + * is ignored (it's only there to allow using standard close() as the func). + */ +int mp_input_add_cmd_fd(struct input_ctx *ictx, int fd, int select, + int read_func(int fd, char *dest, int size), + int close_func(int fd)); + +/* The args are similar to the cmd version above, except you must give + * a read_func, and it should return key codes (ASCII plus keycodes.h). + */ +int mp_input_add_key_fd(struct input_ctx *ictx, int fd, int select, + int read_func(void *ctx, int fd), + int close_func(int fd), void *ctx); + +// Process keyboard input. code is a key code from keycodes.h, possibly +// with modifiers applied. MP_INPUT_RELEASE_ALL is also a valid value. +void mp_input_put_key(struct input_ctx *ictx, int code); + +// Like mp_input_put_key(), but process all UTF-8 characters in the given +// string as key events. +void mp_input_put_key_utf8(struct input_ctx *ictx, int mods, struct bstr t); + +// Update mouse position (in window coordinates). +void mp_input_set_mouse_pos(struct input_ctx *ictx, int x, int y); + +void mp_input_get_mouse_pos(struct input_ctx *ictx, int *x, int *y); + +// As for the cmd one you usually don't need this function. +void mp_input_rm_key_fd(struct input_ctx *ictx, int fd); + +// Get input key from its name. +int mp_input_get_key_from_name(const char *name); + +// Add a command to the command queue. +int mp_input_queue_cmd(struct input_ctx *ictx, struct mp_cmd *cmd); + +/* Return next available command, or sleep up to "time" ms if none is + * available. If "peek_only" is true return a reference to the command + * but leave it queued. + */ +struct mp_cmd *mp_input_get_cmd(struct input_ctx *ictx, int time, + int peek_only); + +// Parse text and return corresponding struct mp_cmd. +// The location parameter is for error messages. +struct mp_cmd *mp_input_parse_cmd(bstr str, const char *location); + +// After getting a command from mp_input_get_cmd you need to free it using this +// function +void mp_cmd_free(struct mp_cmd *cmd); + +// This creates a copy of a command (used by the auto repeat stuff). +struct mp_cmd *mp_cmd_clone(struct mp_cmd *cmd); + +// Set current input section. The section is appended on top of the list of +// active sections, so its bindings are considered first. If the section was +// already active, it's moved to the top as well. +// name==NULL will behave as if name=="default" +// flags is a bitfield of enum mp_input_section_flags values +void mp_input_enable_section(struct input_ctx *ictx, char *name, int flags); + +// Undo mp_input_enable_section(). +// name==NULL will behave as if name=="default" +void mp_input_disable_section(struct input_ctx *ictx, char *name); + +// Like mp_input_set_section(ictx, ..., 0) for all sections. +void mp_input_disable_all_sections(struct input_ctx *ictx); + +// Set the contents of an input section. +// name: name of the section, for mp_input_set_section() etc. +// location: location string (like filename) for error reporting +// contents: list of keybindings, like input.conf +// a value of NULL deletes the section +// builtin: create as builtin section; this means if the user defines bindings +// using "{name}", they won't be ignored or overwritten - instead, +// they are preferred to the bindings defined with this call +// If the section already exists, its bindings are removed and replaced. +void mp_input_define_section(struct input_ctx *ictx, char *name, char *location, + char *contents, bool builtin); + +// Define where on the screen the named input section should receive. +// Setting a rectangle of size 0 unsets the mouse area. +// A rectangle with negative size disables mouse input for this section. +void mp_input_set_section_mouse_area(struct input_ctx *ictx, char *name, + int x0, int y0, int x1, int y1); + +// Used to detect mouse movement. +unsigned int mp_input_get_mouse_event_counter(struct input_ctx *ictx); + +// Test whether there is any input section which wants to receive events. +// Note that the mouse event is always delivered, even if this returns false. +bool mp_input_test_mouse_active(struct input_ctx *ictx, int x, int y); + +// Whether input.c wants mouse drag events at this mouse position. If this +// returns false, some VOs will initiate window dragging. +bool mp_input_test_dragging(struct input_ctx *ictx, int x, int y); + +// Initialize the input system +struct MPOpts; +struct input_ctx *mp_input_init(struct MPOpts *opts); + +void mp_input_uninit(struct input_ctx *ictx); + +// Wake up sleeping input loop from another thread. +void mp_input_wakeup(struct input_ctx *ictx); + +// Interruptible usleep: (used by demux) +int mp_input_check_interrupt(struct input_ctx *ictx, int time); + +extern int async_quit_request; + +#endif /* MPLAYER_INPUT_H */ diff --git a/mpvcore/input/joystick.c b/mpvcore/input/joystick.c new file mode 100644 index 0000000000..e8330ffaeb --- /dev/null +++ b/mpvcore/input/joystick.c @@ -0,0 +1,162 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include "joystick.h" +#include "input.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <errno.h> + +#include "core/mp_msg.h" +#include "keycodes.h" + +#ifndef JOY_AXIS_DELTA +#define JOY_AXIS_DELTA 500 +#endif + +#ifndef JS_DEV +#define JS_DEV "/dev/input/js0" +#endif + +#include <linux/joystick.h> + +int axis[256]; +int btns = 0; + +int mp_input_joystick_init(char* dev) { + int fd,l=0; + int initialized = 0; + struct js_event ev; + + mp_tmsg(MSGT_INPUT,MSGL_V,"Opening joystick device %s\n",dev ? dev : JS_DEV); + + fd = open( dev ? dev : JS_DEV , O_RDONLY | O_NONBLOCK ); + if(fd < 0) { + mp_tmsg(MSGT_INPUT,MSGL_ERR,"Can't open joystick device %s: %s\n",dev ? dev : JS_DEV,strerror(errno)); + return -1; + } + + while(! initialized) { + l = 0; + while((unsigned int)l < sizeof(struct js_event)) { + int r = read(fd,((char*)&ev)+l,sizeof(struct js_event)-l); + if(r < 0) { + if(errno == EINTR) + continue; + else if(errno == EAGAIN) { + initialized = 1; + break; + } + mp_tmsg(MSGT_INPUT,MSGL_ERR,"Error while reading joystick device: %s\n",strerror(errno)); + close(fd); + return -1; + } + l += r; + } + if((unsigned int)l < sizeof(struct js_event)) { + if(l > 0) + mp_tmsg(MSGT_INPUT,MSGL_WARN,"Joystick: We lose %d bytes of data\n",l); + break; + } + if(ev.type == JS_EVENT_BUTTON) + btns |= (ev.value << ev.number); + if(ev.type == JS_EVENT_AXIS) + axis[ev.number] = ev.value; + } + + return fd; +} + +int mp_input_joystick_read(void *ctx, int fd) { + struct js_event ev; + int l=0; + + while((unsigned int)l < sizeof(struct js_event)) { + int r = read(fd,&ev+l,sizeof(struct js_event)-l); + if(r <= 0) { + if(errno == EINTR) + continue; + else if(errno == EAGAIN) + return MP_INPUT_NOTHING; + if( r < 0) + mp_tmsg(MSGT_INPUT,MSGL_ERR,"Error while reading joystick device: %s\n",strerror(errno)); + else + mp_tmsg(MSGT_INPUT,MSGL_ERR,"Error while reading joystick device: %s\n","EOF"); + return MP_INPUT_DEAD; + } + l += r; + } + + if((unsigned int)l < sizeof(struct js_event)) { + if(l > 0) + mp_tmsg(MSGT_INPUT,MSGL_WARN,"Joystick: We lose %d bytes of data\n",l); + return MP_INPUT_NOTHING; + } + + if(ev.type & JS_EVENT_INIT) { + mp_tmsg(MSGT_INPUT,MSGL_WARN,"Joystick: warning init event, we have lost sync with driver.\n"); + ev.type &= ~JS_EVENT_INIT; + if(ev.type == JS_EVENT_BUTTON) { + int s = (btns >> ev.number) & 1; + if(s == ev.value) // State is the same : ignore + return MP_INPUT_NOTHING; + } + if(ev.type == JS_EVENT_AXIS) { + if( ( axis[ev.number] == 1 && ev.value > JOY_AXIS_DELTA) || + (axis[ev.number] == -1 && ev.value < -JOY_AXIS_DELTA) || + (axis[ev.number] == 0 && ev.value >= -JOY_AXIS_DELTA && ev.value <= JOY_AXIS_DELTA) + ) // State is the same : ignore + return MP_INPUT_NOTHING; + } + } + + if(ev.type & JS_EVENT_BUTTON) { + btns &= ~(1 << ev.number); + btns |= (ev.value << ev.number); + if(ev.value == 1) + return (MP_JOY_BTN0 + ev.number) | MP_KEY_STATE_DOWN; + else + return (MP_JOY_BTN0 + ev.number) | MP_KEY_STATE_UP; + } else if(ev.type & JS_EVENT_AXIS) { + if(ev.value < -JOY_AXIS_DELTA && axis[ev.number] != -1) { + axis[ev.number] = -1; + return (MP_JOY_AXIS0_MINUS+(2*ev.number)) | MP_KEY_STATE_DOWN; + } else if(ev.value > JOY_AXIS_DELTA && axis[ev.number] != 1) { + axis[ev.number] = 1; + return (MP_JOY_AXIS0_PLUS+(2*ev.number)) | MP_KEY_STATE_DOWN; + } else if(ev.value <= JOY_AXIS_DELTA && ev.value >= -JOY_AXIS_DELTA && axis[ev.number] != 0) { + int r = axis[ev.number] == 1 ? MP_JOY_AXIS0_PLUS+(2*ev.number) : MP_JOY_AXIS0_MINUS+(2*ev.number); + axis[ev.number] = 0; + return r | MP_KEY_STATE_UP; + } else + return MP_INPUT_NOTHING; + } else { + mp_tmsg(MSGT_INPUT,MSGL_WARN,"Joystick warning unknown event type %d\n",ev.type); + return MP_INPUT_ERROR; + } + + return MP_INPUT_NOTHING; +} diff --git a/mpvcore/input/joystick.h b/mpvcore/input/joystick.h new file mode 100644 index 0000000000..b9480eb686 --- /dev/null +++ b/mpvcore/input/joystick.h @@ -0,0 +1,26 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_JOYSTICK_H +#define MPLAYER_JOYSTICK_H + +int mp_input_joystick_init(char* dev); + +int mp_input_joystick_read(void *ctx, int fd); + +#endif /* MPLAYER_JOYSTICK_H */ diff --git a/mpvcore/input/keycodes.h b/mpvcore/input/keycodes.h new file mode 100644 index 0000000000..d91465f3be --- /dev/null +++ b/mpvcore/input/keycodes.h @@ -0,0 +1,247 @@ +/* + * KEY code definitions for keys/events not passed by ASCII value + * + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_KEYCODES_H +#define MPLAYER_KEYCODES_H + +#define MP_KEY_BASE (1<<21) + +#define MP_KEY_ENTER 13 +#define MP_KEY_TAB 9 + +/* Control keys */ +#define MP_KEY_BACKSPACE (MP_KEY_BASE+0) +#define MP_KEY_DELETE (MP_KEY_BASE+1) +#define MP_KEY_INSERT (MP_KEY_BASE+2) +#define MP_KEY_HOME (MP_KEY_BASE+3) +#define MP_KEY_END (MP_KEY_BASE+4) +#define MP_KEY_PAGE_UP (MP_KEY_BASE+5) +#define MP_KEY_PAGE_DOWN (MP_KEY_BASE+6) +#define MP_KEY_ESC (MP_KEY_BASE+7) +#define MP_KEY_PRINT (MP_KEY_BASE+8) + +/* Control keys short name */ +#define MP_KEY_BS MP_KEY_BACKSPACE +#define MP_KEY_DEL MP_KEY_DELETE +#define MP_KEY_INS MP_KEY_INSERT +#define MP_KEY_PGUP MP_KEY_PAGE_UP +#define MP_KEY_PGDOWN MP_KEY_PAGE_DOWN +#define MP_KEY_PGDWN MP_KEY_PAGE_DOWN + +/* Cursor movement */ +#define MP_KEY_CRSR (MP_KEY_BASE+0x10) +#define MP_KEY_RIGHT (MP_KEY_CRSR+0) +#define MP_KEY_LEFT (MP_KEY_CRSR+1) +#define MP_KEY_DOWN (MP_KEY_CRSR+2) +#define MP_KEY_UP (MP_KEY_CRSR+3) + +/* Multimedia keyboard/remote keys */ +#define MP_KEY_MM_BASE (MP_KEY_BASE+0x20) +#define MP_KEY_POWER (MP_KEY_MM_BASE+0) +#define MP_KEY_MENU (MP_KEY_MM_BASE+1) +#define MP_KEY_PLAY (MP_KEY_MM_BASE+2) +#define MP_KEY_PAUSE (MP_KEY_MM_BASE+3) +#define MP_KEY_PLAYPAUSE (MP_KEY_MM_BASE+4) +#define MP_KEY_STOP (MP_KEY_MM_BASE+5) +#define MP_KEY_FORWARD (MP_KEY_MM_BASE+6) +#define MP_KEY_REWIND (MP_KEY_MM_BASE+7) +#define MP_KEY_NEXT (MP_KEY_MM_BASE+8) +#define MP_KEY_PREV (MP_KEY_MM_BASE+9) +#define MP_KEY_VOLUME_UP (MP_KEY_MM_BASE+10) +#define MP_KEY_VOLUME_DOWN (MP_KEY_MM_BASE+11) +#define MP_KEY_MUTE (MP_KEY_MM_BASE+12) + +/* Function keys */ +#define MP_KEY_F (MP_KEY_BASE+0x40) + +/* Keypad keys */ +#define MP_KEY_KEYPAD (MP_KEY_BASE+0x60) +#define MP_KEY_KP0 (MP_KEY_KEYPAD+0) +#define MP_KEY_KP1 (MP_KEY_KEYPAD+1) +#define MP_KEY_KP2 (MP_KEY_KEYPAD+2) +#define MP_KEY_KP3 (MP_KEY_KEYPAD+3) +#define MP_KEY_KP4 (MP_KEY_KEYPAD+4) +#define MP_KEY_KP5 (MP_KEY_KEYPAD+5) +#define MP_KEY_KP6 (MP_KEY_KEYPAD+6) +#define MP_KEY_KP7 (MP_KEY_KEYPAD+7) +#define MP_KEY_KP8 (MP_KEY_KEYPAD+8) +#define MP_KEY_KP9 (MP_KEY_KEYPAD+9) +#define MP_KEY_KPDEC (MP_KEY_KEYPAD+10) +#define MP_KEY_KPINS (MP_KEY_KEYPAD+11) +#define MP_KEY_KPDEL (MP_KEY_KEYPAD+12) +#define MP_KEY_KPENTER (MP_KEY_KEYPAD+13) + + +// Joystick input module +#define MP_JOY_BASE (MP_KEY_BASE+0x70) +#define MP_JOY_AXIS0_PLUS (MP_JOY_BASE+0) +#define MP_JOY_AXIS0_MINUS (MP_JOY_BASE+1) +#define MP_JOY_AXIS1_PLUS (MP_JOY_BASE+2) +#define MP_JOY_AXIS1_MINUS (MP_JOY_BASE+3) +#define MP_JOY_AXIS2_PLUS (MP_JOY_BASE+4) +#define MP_JOY_AXIS2_MINUS (MP_JOY_BASE+5) +#define MP_JOY_AXIS3_PLUS (MP_JOY_BASE+6) +#define MP_JOY_AXIS3_MINUS (MP_JOY_BASE+7) +#define MP_JOY_AXIS4_PLUS (MP_JOY_BASE+8) +#define MP_JOY_AXIS4_MINUS (MP_JOY_BASE+9) +#define MP_JOY_AXIS5_PLUS (MP_JOY_BASE+10) +#define MP_JOY_AXIS5_MINUS (MP_JOY_BASE+11) +#define MP_JOY_AXIS6_PLUS (MP_JOY_BASE+12) +#define MP_JOY_AXIS6_MINUS (MP_JOY_BASE+13) +#define MP_JOY_AXIS7_PLUS (MP_JOY_BASE+14) +#define MP_JOY_AXIS7_MINUS (MP_JOY_BASE+15) +#define MP_JOY_AXIS8_PLUS (MP_JOY_BASE+16) +#define MP_JOY_AXIS8_MINUS (MP_JOY_BASE+17) +#define MP_JOY_AXIS9_PLUS (MP_JOY_BASE+18) +#define MP_JOY_AXIS9_MINUS (MP_JOY_BASE+19) + +#define MP_JOY_BTN_BASE ((MP_KEY_BASE+0x90)|MP_NO_REPEAT_KEY) +#define MP_JOY_BTN0 (MP_JOY_BTN_BASE+0) +#define MP_JOY_BTN1 (MP_JOY_BTN_BASE+1) +#define MP_JOY_BTN2 (MP_JOY_BTN_BASE+2) +#define MP_JOY_BTN3 (MP_JOY_BTN_BASE+3) +#define MP_JOY_BTN4 (MP_JOY_BTN_BASE+4) +#define MP_JOY_BTN5 (MP_JOY_BTN_BASE+5) +#define MP_JOY_BTN6 (MP_JOY_BTN_BASE+6) +#define MP_JOY_BTN7 (MP_JOY_BTN_BASE+7) +#define MP_JOY_BTN8 (MP_JOY_BTN_BASE+8) +#define MP_JOY_BTN9 (MP_JOY_BTN_BASE+9) + + +// Mouse events from VOs +#define MP_MOUSE_BASE ((MP_KEY_BASE+0xA0)|MP_NO_REPEAT_KEY|MP_KEY_EMIT_ON_UP) +#define MP_MOUSE_BTN0 (MP_MOUSE_BASE+0) +#define MP_MOUSE_BTN1 (MP_MOUSE_BASE+1) +#define MP_MOUSE_BTN2 (MP_MOUSE_BASE+2) +#define MP_MOUSE_BTN3 (MP_MOUSE_BASE+3) +#define MP_MOUSE_BTN4 (MP_MOUSE_BASE+4) +#define MP_MOUSE_BTN5 (MP_MOUSE_BASE+5) +#define MP_MOUSE_BTN6 (MP_MOUSE_BASE+6) +#define MP_MOUSE_BTN7 (MP_MOUSE_BASE+7) +#define MP_MOUSE_BTN8 (MP_MOUSE_BASE+8) +#define MP_MOUSE_BTN9 (MP_MOUSE_BASE+9) +#define MP_MOUSE_BTN10 (MP_MOUSE_BASE+10) +#define MP_MOUSE_BTN11 (MP_MOUSE_BASE+11) +#define MP_MOUSE_BTN12 (MP_MOUSE_BASE+12) +#define MP_MOUSE_BTN13 (MP_MOUSE_BASE+13) +#define MP_MOUSE_BTN14 (MP_MOUSE_BASE+14) +#define MP_MOUSE_BTN15 (MP_MOUSE_BASE+15) +#define MP_MOUSE_BTN16 (MP_MOUSE_BASE+16) +#define MP_MOUSE_BTN17 (MP_MOUSE_BASE+17) +#define MP_MOUSE_BTN18 (MP_MOUSE_BASE+18) +#define MP_MOUSE_BTN19 (MP_MOUSE_BASE+19) +#define MP_MOUSE_BTN_END (MP_MOUSE_BASE+20) + +#define MP_KEY_IS_MOUSE_BTN_SINGLE(code) \ + ((code) >= MP_MOUSE_BASE && (code) < MP_MOUSE_BTN_END) + +#define MP_MOUSE_BASE_DBL ((MP_KEY_BASE+0xC0)|MP_NO_REPEAT_KEY) +#define MP_MOUSE_BTN0_DBL (MP_MOUSE_BASE_DBL+0) +#define MP_MOUSE_BTN1_DBL (MP_MOUSE_BASE_DBL+1) +#define MP_MOUSE_BTN2_DBL (MP_MOUSE_BASE_DBL+2) +#define MP_MOUSE_BTN3_DBL (MP_MOUSE_BASE_DBL+3) +#define MP_MOUSE_BTN4_DBL (MP_MOUSE_BASE_DBL+4) +#define MP_MOUSE_BTN5_DBL (MP_MOUSE_BASE_DBL+5) +#define MP_MOUSE_BTN6_DBL (MP_MOUSE_BASE_DBL+6) +#define MP_MOUSE_BTN7_DBL (MP_MOUSE_BASE_DBL+7) +#define MP_MOUSE_BTN8_DBL (MP_MOUSE_BASE_DBL+8) +#define MP_MOUSE_BTN9_DBL (MP_MOUSE_BASE_DBL+9) +#define MP_MOUSE_BTN10_DBL (MP_MOUSE_BASE_DBL+10) +#define MP_MOUSE_BTN11_DBL (MP_MOUSE_BASE_DBL+11) +#define MP_MOUSE_BTN12_DBL (MP_MOUSE_BASE_DBL+12) +#define MP_MOUSE_BTN13_DBL (MP_MOUSE_BASE_DBL+13) +#define MP_MOUSE_BTN14_DBL (MP_MOUSE_BASE_DBL+14) +#define MP_MOUSE_BTN15_DBL (MP_MOUSE_BASE_DBL+15) +#define MP_MOUSE_BTN16_DBL (MP_MOUSE_BASE_DBL+16) +#define MP_MOUSE_BTN17_DBL (MP_MOUSE_BASE_DBL+17) +#define MP_MOUSE_BTN18_DBL (MP_MOUSE_BASE_DBL+18) +#define MP_MOUSE_BTN19_DBL (MP_MOUSE_BASE_DBL+19) +#define MP_MOUSE_BTN_DBL_END (MP_MOUSE_BASE_DBL+20) + +#define MP_KEY_IS_MOUSE_BTN_DBL(code) \ + ((code) >= MP_MOUSE_BTN0_DBL && (code) < MP_MOUSE_BTN_DBL_END) + +// Apple Remote input module +#define MP_AR_BASE (MP_KEY_BASE+0xE0) +#define MP_AR_PLAY (MP_AR_BASE + 0) +#define MP_AR_PLAY_HOLD (MP_AR_BASE + 1) +#define MP_AR_CENTER (MP_AR_BASE + 2) +#define MP_AR_CENTER_HOLD (MP_AR_BASE + 3) +#define MP_AR_NEXT (MP_AR_BASE + 4) +#define MP_AR_NEXT_HOLD (MP_AR_BASE + 5) +#define MP_AR_PREV (MP_AR_BASE + 6) +#define MP_AR_PREV_HOLD (MP_AR_BASE + 7) +#define MP_AR_MENU (MP_AR_BASE + 8) +#define MP_AR_MENU_HOLD (MP_AR_BASE + 9) +#define MP_AR_VUP (MP_AR_BASE + 10) +#define MP_AR_VUP_HOLD (MP_AR_BASE + 11) +#define MP_AR_VDOWN (MP_AR_BASE + 12) +#define MP_AR_VDOWN_HOLD (MP_AR_BASE + 13) + +// Apple Media Keys input module +#define MP_MK_BASE (MP_KEY_BASE+0xF0) +#define MP_MK_PLAY (MP_MK_BASE + 0) +#define MP_MK_PREV (MP_MK_BASE + 1) +#define MP_MK_NEXT (MP_MK_BASE + 2) + +/* Special keys */ +#define MP_KEY_INTERN (MP_KEY_BASE+0x1000) +#define MP_KEY_CLOSE_WIN (MP_KEY_INTERN+0) +// Generated by input.c (VOs use mp_input_set_mouse_pos()) +#define MP_KEY_MOUSE_MOVE ((MP_KEY_INTERN+1)|MP_NO_REPEAT_KEY) +#define MP_KEY_MOUSE_LEAVE ((MP_KEY_INTERN+2)|MP_NO_REPEAT_KEY) + + +#define MP_KEY_DEPENDS_ON_MOUSE_POS(code) \ + (MP_KEY_IS_MOUSE_BTN_SINGLE(code) || MP_KEY_IS_MOUSE_BTN_DBL(code) || \ + (code) == MP_KEY_MOUSE_MOVE) + +/* Modifiers added to individual keys */ +#define MP_KEY_MODIFIER_SHIFT (1<<22) +#define MP_KEY_MODIFIER_CTRL (1<<23) +#define MP_KEY_MODIFIER_ALT (1<<24) +#define MP_KEY_MODIFIER_META (1<<25) + +#define MP_KEY_MODIFIER_MASK (MP_KEY_MODIFIER_SHIFT | MP_KEY_MODIFIER_CTRL | \ + MP_KEY_MODIFIER_ALT | MP_KEY_MODIFIER_META | \ + MP_KEY_STATE_DOWN | MP_KEY_STATE_UP) + +// Flag for key events. Multiple down events are idempotent. Release keys by +// sending the key code with KEY_STATE_UP set, or by sending +// MP_INPUT_RELEASE_ALL as key code. +#define MP_KEY_STATE_DOWN (1<<26) + +// Flag for key events. Releases a key previously held down with +// MP_KEY_STATE_DOWN. Do not sending redundant UP events and do not forget to +// release keys at all with UP. If input is unreliable, use MP_INPUT_RELEASE_ALL +// or don't use MP_KEY_STATE_DOWN in the first place. +#define MP_KEY_STATE_UP (1<<27) + +// The following flags are not modifiers, but are part of the keycode itself. + +// Emit a command even on key-up (normally key-up is ignored). The command +// handling code has to ignore unwanted commands specifically. +#define MP_KEY_EMIT_ON_UP (1<<28) + +// Use this when the key shouldn't be auto-repeated (like mouse buttons) +// Also means both key-down key-up events produce emit bound commands. +#define MP_NO_REPEAT_KEY (1<<29) + +#endif /* MPLAYER_KEYCODES_H */ diff --git a/mpvcore/input/lirc.c b/mpvcore/input/lirc.c new file mode 100644 index 0000000000..699168d239 --- /dev/null +++ b/mpvcore/input/lirc.c @@ -0,0 +1,123 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <lirc/lirc_client.h> +#include <errno.h> +#include <fcntl.h> +#include <stdio.h> +#include <string.h> +#include <unistd.h> +#include <stdlib.h> + +#include "core/mp_msg.h" +#include "input.h" +#include "lirc.h" + +static struct lirc_config *lirc_config; +char *lirc_configfile; + +static char* cmd_buf = NULL; + +int +mp_input_lirc_init(void) { + int lirc_sock; + int mode; + + mp_tmsg(MSGT_LIRC,MSGL_V,"Setting up LIRC support...\n"); + if((lirc_sock=lirc_init("mpv",0))==-1){ + mp_tmsg(MSGT_LIRC,MSGL_V,"Failed to open LIRC support. You will not be able to use your remote control.\n"); + return -1; + } + + mode = fcntl(lirc_sock, F_GETFL); + if (mode < 0 || fcntl(lirc_sock, F_SETFL, mode | O_NONBLOCK) < 0) { + mp_msg(MSGT_LIRC, MSGL_ERR, "setting non-blocking mode failed: %s\n", + strerror(errno)); + lirc_deinit(); + return -1; + } + + if(lirc_readconfig( lirc_configfile,&lirc_config,NULL )!=0 ){ + mp_tmsg(MSGT_LIRC,MSGL_ERR,"Failed to read LIRC config file %s.\n", + lirc_configfile == NULL ? "~/.lircrc" : lirc_configfile); + lirc_deinit(); + return -1; + } + + return lirc_sock; +} + +int mp_input_lirc_read(int fd,char* dest, int s) { + int r,cl = 0; + char *code = NULL,*c = NULL; + + // We have something in the buffer return it + if(cmd_buf != NULL) { + int l = strlen(cmd_buf), w = l > s ? s : l; + memcpy(dest,cmd_buf,w); + l -= w; + if(l > 0) + memmove(cmd_buf,&cmd_buf[w],l+1); + else { + free(cmd_buf); + cmd_buf = NULL; + } + return w; + } + + // Nothing in the buffer, poll the lirc fd + if(lirc_nextcode(&code) != 0) { + mp_msg(MSGT_LIRC,MSGL_ERR,"Lirc error :(\n"); + return MP_INPUT_DEAD; + } + + if(!code) return MP_INPUT_NOTHING; + + // We put all cmds in a single buffer separated by \n + while((r = lirc_code2char(lirc_config,code,&c))==0 && c!=NULL) { + int l = strlen(c); + if(l <= 0) + continue; + cmd_buf = realloc(cmd_buf,cl+l+2); + memcpy(&cmd_buf[cl],c,l); + cl += l+1; + cmd_buf[cl-1] = '\n'; + cmd_buf[cl] = '\0'; + } + + free(code); + + if(r < 0) + return MP_INPUT_DEAD; + else if(cmd_buf) // return the first command in the buffer + return mp_input_lirc_read(fd,dest,s); + else + return MP_INPUT_RETRY; + +} + +int mp_input_lirc_close(int fd) +{ + free(cmd_buf); + cmd_buf = NULL; + lirc_freeconfig(lirc_config); + lirc_deinit(); + return 0; +} diff --git a/mpvcore/input/lirc.h b/mpvcore/input/lirc.h new file mode 100644 index 0000000000..ac1937f91d --- /dev/null +++ b/mpvcore/input/lirc.h @@ -0,0 +1,30 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_LIRC_H +#define MPLAYER_LIRC_H + +int +mp_input_lirc_init(void); + +int +mp_input_lirc_read(int fd,char* dest, int s); + +int mp_input_lirc_close(int fd); + +#endif /* MPLAYER_LIRC_H */ diff --git a/mpvcore/m_config.c b/mpvcore/m_config.c new file mode 100644 index 0000000000..f362b43836 --- /dev/null +++ b/mpvcore/m_config.c @@ -0,0 +1,777 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/// \file +/// \ingroup Config + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <errno.h> +#include <string.h> +#include <assert.h> +#include <stdbool.h> + +#include "talloc.h" + +#include "m_config.h" +#include "core/m_option.h" +#include "core/mp_msg.h" + +// Profiles allow to predefine some sets of options that can then +// be applied later on with the internal -profile option. +#define MAX_PROFILE_DEPTH 20 + +struct m_profile { + struct m_profile *next; + char *name; + char *desc; + int num_opts; + // Option/value pair array. + char **opts; +}; + +// In the file local case, this contains the old global value. +struct m_opt_backup { + struct m_opt_backup *next; + struct m_config_option *co; + void *backup; +}; + +static int parse_include(struct m_config *config, struct bstr param, bool set, + int flags) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + if (!set) + return 1; + char *filename = bstrdup0(NULL, param); + config->includefunc(config, filename, flags); + talloc_free(filename); + return 1; +} + +static int parse_profile(struct m_config *config, const struct m_option *opt, + struct bstr name, struct bstr param, bool set, int flags) +{ + if (!bstrcmp0(param, "help")) { + struct m_profile *p; + if (!config->profiles) { + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, + "No profiles have been defined.\n"); + return M_OPT_EXIT - 1; + } + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, "Available profiles:\n"); + for (p = config->profiles; p; p = p->next) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\t%s\t%s\n", p->name, + p->desc ? p->desc : ""); + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\n"); + return M_OPT_EXIT - 1; + } + + char **list = NULL; + int r = m_option_type_string_list.parse(opt, name, param, &list); + if (r < 0) + return r; + if (!list || !list[0]) + return M_OPT_INVALID; + for (int i = 0; list[i]; i++) { + struct m_profile *p = m_config_get_profile0(config, list[i]); + if (!p) { + mp_tmsg(MSGT_CFGPARSER, MSGL_WARN, "Unknown profile '%s'.\n", + list[i]); + r = M_OPT_INVALID; + } else if (set) + m_config_set_profile(config, p, flags); + } + m_option_free(opt, &list); + return r; +} + +static int show_profile(struct m_config *config, bstr param) +{ + struct m_profile *p; + int i, j; + if (!param.len) + return M_OPT_MISSING_PARAM; + if (!(p = m_config_get_profile(config, param))) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, "Unknown profile '%.*s'.\n", + BSTR_P(param)); + return M_OPT_EXIT - 1; + } + if (!config->profile_depth) + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, "Profile %s: %s\n", p->name, + p->desc ? p->desc : ""); + config->profile_depth++; + for (i = 0; i < p->num_opts; i++) { + char spc[config->profile_depth + 1]; + for (j = 0; j < config->profile_depth; j++) + spc[j] = ' '; + spc[config->profile_depth] = '\0'; + + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "%s%s=%s\n", spc, + p->opts[2 * i], p->opts[2 * i + 1]); + + if (config->profile_depth < MAX_PROFILE_DEPTH + && !strcmp(p->opts[2*i], "profile")) { + char *e, *list = p->opts[2 * i + 1]; + while ((e = strchr(list, ','))) { + int l = e - list; + char tmp[l+1]; + if (!l) + continue; + memcpy(tmp, list, l); + tmp[l] = '\0'; + show_profile(config, bstr0(tmp)); + list = e + 1; + } + if (list[0] != '\0') + show_profile(config, bstr0(list)); + } + } + config->profile_depth--; + if (!config->profile_depth) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\n"); + return M_OPT_EXIT - 1; +} + +static int list_options(struct m_config *config) +{ + m_config_print_option_list(config); + return M_OPT_EXIT; +} + +// The memcpys are supposed to work around the struct aliasing violation, +// that would result if we just dereferenced a void** (where the void** is +// actually casted from struct some_type* ). +static void *substruct_read_ptr(void *ptr) +{ + void *res; + memcpy(&res, ptr, sizeof(void*)); + return res; +} +static void substruct_write_ptr(void *ptr, void *val) +{ + memcpy(ptr, &val, sizeof(void*)); +} + +static struct m_config_option *m_config_add_option(struct m_config *config, + const char *prefix, + struct m_config_option *parent, + const struct m_option *arg); + +static void add_options(struct m_config *config, + struct m_config_option *parent, + const struct m_option *defs); + +static int config_destroy(void *p) +{ + struct m_config *config = p; + m_config_restore_backups(config); + for (struct m_config_option *copt = config->opts; copt; copt = copt->next) + m_option_free(copt->opt, copt->data); + return 0; +} + +struct m_config *m_config_new(void *talloc_parent, size_t size, + const void *defaults, + const struct m_option *options, + const char *suboptinit) +{ + struct m_config *config = talloc(talloc_parent, struct m_config); + talloc_set_destructor(config, config_destroy); + *config = (struct m_config) { + .optstruct_size = size, + .optstruct_defaults = defaults, + .options = options, + .suboptinit = suboptinit, + }; + if (size) { // size==0 means a dummy object is created + config->optstruct = talloc_zero_size(config, size); + if (defaults) + memcpy(config->optstruct, defaults, size); + if (options) + add_options(config, NULL, options); + } + if (suboptinit) { + bstr s = bstr0(suboptinit); + int r = m_obj_parse_sub_config(bstr0("internal"), bstr0("-"), &s, + config, 0, NULL); + if (r < 0 || s.len > 0) + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Internal error: preset broken\n"); + } + return config; +} + +struct m_config *m_config_from_obj_desc(void *talloc_parent, + struct m_obj_desc *desc) +{ + return m_config_new(talloc_parent, desc->priv_size, desc->priv_defaults, + desc->options, desc->init_options); +} + +int m_config_set_obj_params(struct m_config *conf, char **args) +{ + for (int n = 0; args && args[n * 2 + 0]; n++) { + int r = m_config_set_option(conf, bstr0(args[n * 2 + 0]), + bstr0(args[n * 2 + 1])); + if (r < 0) + return r; + } + return 0; +} + +int m_config_initialize_obj(struct m_config *config, struct m_obj_desc *desc, + void **ppriv, char ***pargs) +{ + if (desc->priv_size) { + int r = m_config_set_obj_params(config, *pargs); + if (r < 0) + return r; + *ppriv = config->optstruct; + *pargs = NULL; + } else if (*pargs && !strcmp((*pargs)[0], "_oldargs_")) { + // Handle things which still use the old subopt parser + *pargs = (char **)((*pargs)[1]); + } else { + *pargs = NULL; + } + return 0; +} + +static void ensure_backup(struct m_config *config, struct m_config_option *co) +{ + if (co->opt->type->flags & M_OPT_TYPE_HAS_CHILD) + return; + if (co->opt->flags & M_OPT_GLOBAL) + return; + if (!co->data) + return; + for (struct m_opt_backup *cur = config->backup_opts; cur; cur = cur->next) { + if (cur->co->data == co->data) // comparing data ptr catches aliases + return; + } + struct m_opt_backup *bc = talloc_ptrtype(NULL, bc); + *bc = (struct m_opt_backup) { + .co = co, + .backup = talloc_zero_size(bc, co->opt->type->size), + }; + m_option_copy(co->opt, bc->backup, co->data); + bc->next = config->backup_opts; + config->backup_opts = bc; +} + +void m_config_restore_backups(struct m_config *config) +{ + while (config->backup_opts) { + struct m_opt_backup *bc = config->backup_opts; + config->backup_opts = bc->next; + + m_option_copy(bc->co->opt, bc->co->data, bc->backup); + m_option_free(bc->co->opt, bc->backup); + talloc_free(bc); + } +} + +void m_config_backup_opt(struct m_config *config, const char *opt) +{ + struct m_config_option *co = m_config_get_co(config, bstr0(opt)); + if (co) { + ensure_backup(config, co); + } else { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, "Option %s not found.\n", opt); + } +} + +void m_config_backup_all_opts(struct m_config *config) +{ + for (struct m_config_option *co = config->opts; co; co = co->next) + ensure_backup(config, co); +} + +// Given an option --opt, add --no-opt (if applicable). +static void add_negation_option(struct m_config *config, + struct m_config_option *parent, + const struct m_option *opt) +{ + int value; + if (opt->type == CONF_TYPE_FLAG) { + value = opt->min; + } else if (opt->type == CONF_TYPE_CHOICE) { + // Find out whether there's a "no" choice. + // m_option_parse() should be used for this, but it prints + // unsilenceable error messages. + struct m_opt_choice_alternatives *alt = opt->priv; + for ( ; alt->name; alt++) { + if (strcmp(alt->name, "no") == 0) + break; + } + if (!alt->name) + return; + value = alt->value; + } else { + return; + } + struct m_option *no_opt = talloc_ptrtype(config, no_opt); + *no_opt = (struct m_option) { + .name = talloc_asprintf(no_opt, "no-%s", opt->name), + .type = CONF_TYPE_STORE, + .flags = opt->flags & (M_OPT_NOCFG | M_OPT_GLOBAL | M_OPT_PRE_PARSE), + .new = opt->new, + .p = opt->p, + .offset = opt->offset, + .max = value, + }; + struct m_config_option *co = m_config_add_option(config, "", parent, no_opt); + co->is_generated = true; + // Consider a parent option "--sub" and a subopt "opt". Then the above + // call will add "no-opt". Add "--no-sub-opt" too. (This former call will + // also generate "--sub-no-opt", which is not really needed or wanted, but + // is a consequence of supporting "--sub=...:no-opt".) + if (parent && parent->name && strlen(parent->name)) { + no_opt = talloc_memdup(config, no_opt, sizeof(*no_opt)); + no_opt->name = opt->name; + co = m_config_add_option(config, "no-", parent, no_opt); + co->is_generated = true; + } +} + +static void add_options(struct m_config *config, + struct m_config_option *parent, + const struct m_option *defs) +{ + for (int i = 0; defs[i].name; i++) + m_config_add_option(config, "", parent, defs + i); +} + +// Sub-config that adds all its children to the parent. +static bool is_merge_opt(const struct m_option *opt) +{ + return (opt->type->flags & M_OPT_TYPE_HAS_CHILD) && strlen(opt->name) == 0; +} + +static struct m_config_option *m_config_add_option(struct m_config *config, + const char *prefix, + struct m_config_option *parent, + const struct m_option *arg) +{ + assert(config != NULL); + assert(arg != NULL); + + // Allocate a new entry for this option + struct m_config_option *co = talloc_zero(config, struct m_config_option); + co->opt = arg; + + void *optstruct = config->optstruct; + if (parent && (parent->opt->type->flags & M_OPT_TYPE_USE_SUBSTRUCT)) + optstruct = substruct_read_ptr(parent->data); + co->data = arg->new ? (char *)optstruct + arg->offset : arg->p; + + if (parent) { + // Merge case: pretend it has no parent (note that we still must follow + // the "real" parent for accessing struct fields) + co->parent = is_merge_opt(parent->opt) ? parent->parent : parent; + } + + // Fill in the full name + if (co->parent) { + co->name = talloc_asprintf(co, "%s-%s", co->parent->name, arg->name); + } else { + co->name = (char *)arg->name; + } + co->name = talloc_asprintf(co, "%s%s", prefix, co->name); + + // Option with children -> add them + if (arg->type->flags & M_OPT_TYPE_HAS_CHILD) { + if (arg->type->flags & M_OPT_TYPE_USE_SUBSTRUCT) { + const struct m_sub_options *subopts = arg->priv; + if (!substruct_read_ptr(co->data)) { + void *subdata = m_config_alloc_struct(config, subopts); + substruct_write_ptr(co->data, subdata); + } + add_options(config, co, subopts->opts); + } else { + const struct m_option *sub = arg->p; + add_options(config, co, sub); + } + } else { + // Initialize options + if (co->data) { + if (arg->defval) { + // Target data in optstruct is supposed to be cleared (consider + // m_option freeing previously set dynamic data). + m_option_copy(arg, co->data, arg->defval); + } else if (arg->type->flags & M_OPT_TYPE_DYNAMIC) { + // Initialize dynamically managed fields from static data (like + // string options): copy the option into temporary memory, + // clear the original option (to stop m_option from freeing the + // static data), copy it back. + // This would leak memory when done on aliased options. + bool aliased = false; + for (struct m_config_option *i = config->opts; i; i = i->next) { + if (co->data == i->data) { + aliased = true; + break; + } + } + if (!aliased) { + union m_option_value temp = {0}; + m_option_copy(arg, &temp, co->data); + memset(co->data, 0, arg->type->size); + m_option_copy(arg, co->data, &temp); + m_option_free(arg, &temp); + } + } + } + } + + // pretend that merge options don't exist (only their children matter) + if (!is_merge_opt(co->opt)) { + struct m_config_option **last = &config->opts; + while (*last) + last = &(*last)->next; + *last = co; + } + + add_negation_option(config, parent, arg); + + return co; +} + +struct m_config_option *m_config_get_co(const struct m_config *config, + struct bstr name) +{ + struct m_config_option *co; + + for (co = config->opts; co; co = co->next) { + struct bstr coname = bstr0(co->name); + if ((co->opt->type->flags & M_OPT_TYPE_ALLOW_WILDCARD) + && bstr_endswith0(coname, "*")) { + coname.len--; + if (bstrcmp(bstr_splice(name, 0, coname.len), coname) == 0) + return co; + } else if (bstrcmp(coname, name) == 0) + return co; + } + return NULL; +} + +const char *m_config_get_positional_option(const struct m_config *config, int n) +{ + int pos = 0; + for (struct m_config_option *co = config->opts; co; co = co->next) { + if (!co->is_generated) { + if (pos == n) + return co->name; + pos++; + } + } + return NULL; +} + +static int parse_subopts(struct m_config *config, char *name, char *prefix, + struct bstr param, int flags); + +static int m_config_parse_option(struct m_config *config, struct bstr name, + struct bstr param, int flags) +{ + assert(config != NULL); + assert(name.len != 0); + bool set = !(flags & M_SETOPT_CHECK_ONLY); + + struct m_config_option *co = m_config_get_co(config, name); + if (!co) + return M_OPT_UNKNOWN; + + // This is the only mandatory function + assert(co->opt->type->parse); + + if ((flags & M_SETOPT_PRE_PARSE_ONLY) && !(co->opt->flags & M_OPT_PRE_PARSE)) + return 0; + + // Check if this option isn't forbidden in the current mode + if ((flags & M_SETOPT_FROM_CONFIG_FILE) && (co->opt->flags & M_OPT_NOCFG)) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option can't be used in a config file.\n", + BSTR_P(name)); + return M_OPT_INVALID; + } + if (flags & M_SETOPT_BACKUP) { + if (co->opt->flags & M_OPT_GLOBAL) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option is global and can't be set per-file.\n", + BSTR_P(name)); + return M_OPT_INVALID; + } + if (set) + ensure_backup(config, co); + } + + if (config->includefunc && bstr_equals0(name, "include")) + return parse_include(config, param, set, flags); + if (config->use_profiles && bstr_equals0(name, "profile")) + return parse_profile(config, co->opt, name, param, set, flags); + if (config->use_profiles && bstr_equals0(name, "show-profile")) + return show_profile(config, param); + if (bstr_equals0(name, "list-options")) + return list_options(config); + + // Option with children are a bit different to parse + if (co->opt->type->flags & M_OPT_TYPE_HAS_CHILD) { + char prefix[110]; + assert(strlen(co->name) < 100); + sprintf(prefix, "%s-", co->name); + return parse_subopts(config, co->name, prefix, param, flags); + } + + return m_option_parse(co->opt, name, param, set ? co->data : NULL); +} + +static int parse_subopts(struct m_config *config, char *name, char *prefix, + struct bstr param, int flags) +{ + char **lst = NULL; + // Split the argument into child options + int r = m_option_type_subconfig.parse(NULL, bstr0(""), param, &lst); + if (r < 0) + return r; + // Parse the child options + for (int i = 0; lst && lst[2 * i]; i++) { + // Build the full name + char n[110]; + if (snprintf(n, 110, "%s%s", prefix, lst[2 * i]) > 100) + abort(); + r = m_config_parse_option(config,bstr0(n), bstr0(lst[2 * i + 1]), flags); + if (r < 0) { + if (r > M_OPT_EXIT) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, + "Error parsing suboption %s/%s (%s)\n", + name, lst[2 * i], m_option_strerror(r)); + r = M_OPT_INVALID; + } + break; + } + } + talloc_free(lst); + return r; +} + +int m_config_parse_suboptions(struct m_config *config, char *name, + char *subopts) +{ + if (!subopts || !*subopts) + return 0; + int r = parse_subopts(config, name, "", bstr0(subopts), 0); + if (r < 0 && r > M_OPT_EXIT) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, "Error parsing suboption %s (%s)\n", + name, m_option_strerror(r)); + r = M_OPT_INVALID; + } + return r; +} + +int m_config_set_option_ext(struct m_config *config, struct bstr name, + struct bstr param, int flags) +{ + int r = m_config_parse_option(config, name, param, flags); + if (r < 0 && r > M_OPT_EXIT) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, "Error parsing option %.*s (%s)\n", + BSTR_P(name), m_option_strerror(r)); + r = M_OPT_INVALID; + } + return r; +} + +int m_config_set_option(struct m_config *config, struct bstr name, + struct bstr param) +{ + return m_config_set_option_ext(config, name, param, 0); +} + +const struct m_option *m_config_get_option(const struct m_config *config, + struct bstr name) +{ + struct m_config_option *co; + + assert(config != NULL); + + co = m_config_get_co(config, name); + if (co) + return co->opt; + else + return NULL; +} + +int m_config_option_requires_param(struct m_config *config, bstr name) +{ + const struct m_option *opt = m_config_get_option(config, name); + if (opt) { + if (bstr_endswith0(name, "-clr")) + return 0; + return m_option_required_params(opt); + } + return M_OPT_UNKNOWN; +} + +static struct m_config *get_defaults(const struct m_config *config) +{ + return m_config_new(NULL, config->optstruct_size, + config->optstruct_defaults, config->options, + config->suboptinit); +} + +static char *get_option_value_string(const struct m_config *config, + const char *name) +{ + struct m_config_option *co = m_config_get_co(config, bstr0(name)); + if (!co || !co->data) + return NULL; + return m_option_print(co->opt, co->data); +} + +void m_config_print_option_list(const struct m_config *config) +{ + char min[50], max[50]; + struct m_config_option *co; + int count = 0; + + if (!config->opts) + return; + + struct m_config *defaults = get_defaults(config); + + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, "Options:\n\n"); + for (co = config->opts; co; co = co->next) { + const struct m_option *opt = co->opt; + if (opt->type->flags & M_OPT_TYPE_HAS_CHILD) + continue; + if (co->is_generated) + continue; + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " %-30.30s", co->name); + if (opt->type == &m_option_type_choice) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " Choices:"); + struct m_opt_choice_alternatives *alt = opt->priv; + for (int n = 0; alt[n].name; n++) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " %s", alt[n].name); + if (opt->flags & (M_OPT_MIN | M_OPT_MAX)) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " (or an integer)"); + } else { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " %s", co->opt->type->name); + } + if (opt->flags & (M_OPT_MIN | M_OPT_MAX)) { + snprintf(min, sizeof(min), "any"); + snprintf(max, sizeof(max), "any"); + if (opt->flags & M_OPT_MIN) + snprintf(min, sizeof(min), "%.14g", opt->min); + if (opt->flags & M_OPT_MAX) + snprintf(max, sizeof(max), "%.14g", opt->max); + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " (%s to %s)", min, max); + } + char *def = get_option_value_string(defaults, co->name); + if (def) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " (default: %s)", def); + talloc_free(def); + } + if (opt->flags & CONF_GLOBAL) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " [global]"); + if (opt->flags & CONF_NOCFG) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " [nocfg]"); + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\n"); + count++; + } + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, "\nTotal: %d options\n", count); + + talloc_free(defaults); +} + +struct m_profile *m_config_get_profile(const struct m_config *config, bstr name) +{ + for (struct m_profile *p = config->profiles; p; p = p->next) { + if (bstr_equals0(name, p->name)) + return p; + } + return NULL; +} + +struct m_profile *m_config_get_profile0(const struct m_config *config, + char *name) +{ + return m_config_get_profile(config, bstr0(name)); +} + +struct m_profile *m_config_add_profile(struct m_config *config, char *name) +{ + struct m_profile *p = m_config_get_profile0(config, name); + if (p) + return p; + p = talloc_zero(config, struct m_profile); + p->name = talloc_strdup(p, name); + p->next = config->profiles; + config->profiles = p; + return p; +} + +void m_profile_set_desc(struct m_profile *p, char *desc) +{ + talloc_free(p->desc); + p->desc = talloc_strdup(p, desc); +} + +int m_config_set_profile_option(struct m_config *config, struct m_profile *p, + bstr name, bstr val) +{ + int i = m_config_set_option_ext(config, name, val, + M_SETOPT_CHECK_ONLY | + M_SETOPT_FROM_CONFIG_FILE); + if (i < 0) + return i; + p->opts = talloc_realloc(p, p->opts, char *, 2 * (p->num_opts + 2)); + p->opts[p->num_opts * 2] = bstrdup0(p, name); + p->opts[p->num_opts * 2 + 1] = bstrdup0(p, val); + p->num_opts++; + p->opts[p->num_opts * 2] = p->opts[p->num_opts * 2 + 1] = NULL; + return 1; +} + +void m_config_set_profile(struct m_config *config, struct m_profile *p, + int flags) +{ + if (config->profile_depth > MAX_PROFILE_DEPTH) { + mp_tmsg(MSGT_CFGPARSER, MSGL_WARN, + "WARNING: Profile inclusion too deep.\n"); + return; + } + config->profile_depth++; + for (int i = 0; i < p->num_opts; i++) { + m_config_set_option_ext(config, + bstr0(p->opts[2 * i]), + bstr0(p->opts[2 * i + 1]), + flags | M_SETOPT_FROM_CONFIG_FILE); + } + config->profile_depth--; +} + +void *m_config_alloc_struct(void *talloc_parent, + const struct m_sub_options *subopts) +{ + void *substruct = talloc_zero_size(talloc_parent, subopts->size); + if (subopts->defaults) + memcpy(substruct, subopts->defaults, subopts->size); + return substruct; +} diff --git a/mpvcore/m_config.h b/mpvcore/m_config.h new file mode 100644 index 0000000000..c2f88dfe65 --- /dev/null +++ b/mpvcore/m_config.h @@ -0,0 +1,217 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_M_CONFIG_H +#define MPLAYER_M_CONFIG_H + +#include <stddef.h> +#include <stdbool.h> + +#include "core/bstr.h" + +// m_config provides an API to manipulate the config variables in MPlayer. +// It makes use of the Options API to provide a context stack that +// allows saving and later restoring the state of all variables. + +typedef struct m_profile m_profile_t; +struct m_option; +struct m_option_type; +struct m_sub_options; +struct m_obj_desc; + +// Config option +struct m_config_option { + struct m_config_option *next; + bool is_generated : 1; + // Full name (ie option-subopt). + char *name; + // Option description. + const struct m_option *opt; + // Raw value of the option. + void *data; + // If this is a suboption, the option that contains this option. + struct m_config_option *parent; +}; + +// Config object +/** \ingroup Config */ +typedef struct m_config { + // Registered options. + struct m_config_option *opts; // all options, even suboptions + + // List of defined profiles. + struct m_profile *profiles; + // Depth when recursively including profiles. + int profile_depth; + + struct m_opt_backup *backup_opts; + + bool use_profiles; + int (*includefunc)(struct m_config *conf, char *filename, int flags); + + const void *optstruct_defaults; + size_t optstruct_size; + const struct m_option *options; // top-level options + const char *suboptinit; + + void *optstruct; // struct mpopts or other +} m_config_t; + +// Create a new config object. +// talloc_parent: talloc parent context for the m_config allocation +// size: size of the optstruct (where option values are stored) +// defaults: if not NULL, points to a struct of same type as optstruct, which +// contains default values for all options +// options: list of options. Each option defines a member of the optstruct +// and a corresponding option switch or sub-option field. +// suboptinit: if not NULL, initialize the suboption string (used for presets) +// Note that the m_config object will keep pointers to defaults and options. +struct m_config *m_config_new(void *talloc_parent, size_t size, + const void *defaults, + const struct m_option *options, + const char *suboptinit); + +struct m_config *m_config_from_obj_desc(void *talloc_parent, + struct m_obj_desc *desc); + +int m_config_set_obj_params(struct m_config *conf, char **args); + +// Initialize an object (VO/VF/...) in one go, including legacy handling. +// This is pretty specialized, and is just for convenience. +int m_config_initialize_obj(struct m_config *config, struct m_obj_desc *desc, + void **ppriv, char ***pargs); + +// Make sure the option is backed up. If it's already backed up, do nothing. +// All backed up options can be restored with m_config_restore_backups(). +void m_config_backup_opt(struct m_config *config, const char *opt); + +// Call m_config_backup_opt() on all options. +void m_config_backup_all_opts(struct m_config *config); + +// Restore all options backed up with m_config_backup_opt(), and delete the +// backups afterwards. +void m_config_restore_backups(struct m_config *config); + +enum { + M_SETOPT_PRE_PARSE_ONLY = 1, // Silently ignore non-M_OPT_PRE_PARSE opt. + M_SETOPT_CHECK_ONLY = 2, // Don't set, just check name/value + M_SETOPT_FROM_CONFIG_FILE = 4, // Reject M_OPT_NOCFG opt. (print error) + M_SETOPT_BACKUP = 8, // Call m_config_backup_opt() before +}; + +// Set the named option to the given string. +// flags: combination of M_SETOPT_* flags (0 for normal operation) +// Returns >= 0 on success, otherwise see OptionParserReturn. +int m_config_set_option_ext(struct m_config *config, struct bstr name, + struct bstr param, int flags); + +/* Set an option. (Like: m_config_set_option_ext(config, name, param, 0)) + * \param config The config object. + * \param name The option's name. + * \param param The value of the option, can be NULL. + * \return See \ref OptionParserReturn. + */ +int m_config_set_option(struct m_config *config, struct bstr name, + struct bstr param); + +static inline int m_config_set_option0(struct m_config *config, + const char *name, const char *param) +{ + return m_config_set_option(config, bstr0(name), bstr0(param)); +} + +int m_config_parse_suboptions(struct m_config *config, char *name, + char *subopts); + + +/* Get the option matching the given name. + * \param config The config object. + * \param name The option's name. + */ +const struct m_option *m_config_get_option(const struct m_config *config, + struct bstr name); + +struct m_config_option *m_config_get_co(const struct m_config *config, + struct bstr name); + +// Return the n-th option by position. n==0 is the first option. If there are +// less than (n + 1) options, return NULL. +const char *m_config_get_positional_option(const struct m_config *config, int n); + +// Return a hint to the option parser whether a parameter is/may be required. +// The option may still accept empty/non-empty parameters independent from +// this, and this function is useful only for handling ambiguous options like +// flags (e.g. "--a" is ok, "--a=yes" is also ok). +// Returns: error code (<0), or number of expected params (0, 1) +int m_config_option_requires_param(struct m_config *config, bstr name); + +/* Print a list of all registered options. + * \param config The config object. + */ +void m_config_print_option_list(const struct m_config *config); + + +/* Find the profile with the given name. + * \param config The config object. + * \param arg The profile's name. + * \return The profile object or NULL. + */ +struct m_profile *m_config_get_profile0(const struct m_config *config, + char *name); +struct m_profile *m_config_get_profile(const struct m_config *config, bstr name); + +/* Get the profile with the given name, creating it if necessary. + * \param config The config object. + * \param arg The profile's name. + * \return The profile object. + */ +struct m_profile *m_config_add_profile(struct m_config *config, char *name); + +/* Set the description of a profile. + * Used by the config file parser when defining a profile. + * + * \param p The profile object. + * \param arg The profile's name. + */ +void m_profile_set_desc(struct m_profile *p, char *desc); + +/* Add an option to a profile. + * Used by the config file parser when defining a profile. + * + * \param config The config object. + * \param p The profile object. + * \param name The option's name. + * \param val The option's value. + */ +int m_config_set_profile_option(struct m_config *config, struct m_profile *p, + bstr name, bstr val); + +/* Enables profile usage + * Used by the config file parser when loading a profile. + * + * \param config The config object. + * \param p The profile object. + * \param flags M_SETOPT_* bits + */ +void m_config_set_profile(struct m_config *config, struct m_profile *p, + int flags); + +void *m_config_alloc_struct(void *talloc_parent, + const struct m_sub_options *subopts); + +#endif /* MPLAYER_M_CONFIG_H */ diff --git a/mpvcore/m_option.c b/mpvcore/m_option.c new file mode 100644 index 0000000000..41916befeb --- /dev/null +++ b/mpvcore/m_option.c @@ -0,0 +1,2407 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/// \file +/// \ingroup Options + +#include "config.h" + +#include <stdlib.h> +#include <string.h> +#include <math.h> +#include <stdio.h> +#include <stdarg.h> +#include <limits.h> +#include <inttypes.h> +#include <unistd.h> +#include <ctype.h> +#include <assert.h> + +#include <libavutil/common.h> +#include <libavutil/avstring.h> + +#include "talloc.h" +#include "core/mp_common.h" +#include "core/m_option.h" +#include "core/m_config.h" +#include "core/mp_msg.h" + +char *m_option_strerror(int code) +{ + switch (code) { + case M_OPT_UNKNOWN: + return mp_gtext("option not found"); + case M_OPT_MISSING_PARAM: + return mp_gtext("option requires parameter"); + case M_OPT_INVALID: + return mp_gtext("option parameter could not be parsed"); + case M_OPT_OUT_OF_RANGE: + return mp_gtext("parameter is outside values allowed for option"); + case M_OPT_DISALLOW_PARAM: + return mp_gtext("option doesn't take a parameter"); + case M_OPT_PARSER_ERR: + default: + return mp_gtext("parser error"); + } +} + +int m_option_required_params(const m_option_t *opt) +{ + if (((opt->flags & M_OPT_OPTIONAL_PARAM) || + (opt->type->flags & M_OPT_TYPE_OPTIONAL_PARAM))) + return 0; + return 1; +} + +static const struct m_option *m_option_list_findb(const struct m_option *list, + struct bstr name) +{ + for (int i = 0; list[i].name; i++) { + struct bstr lname = bstr0(list[i].name); + if ((list[i].type->flags & M_OPT_TYPE_ALLOW_WILDCARD) + && bstr_endswith0(lname, "*")) { + lname.len--; + if (bstrcmp(bstr_splice(name, 0, lname.len), lname) == 0) + return &list[i]; + } else if (bstrcmp(lname, name) == 0) + return &list[i]; + } + return NULL; +} + +const m_option_t *m_option_list_find(const m_option_t *list, const char *name) +{ + return m_option_list_findb(list, bstr0(name)); +} + +// Default function that just does a memcpy + +static void copy_opt(const m_option_t *opt, void *dst, const void *src) +{ + if (dst && src) + memcpy(dst, src, opt->type->size); +} + +// Flag + +#define VAL(x) (*(int *)(x)) + +static int clamp_flag(const m_option_t *opt, void *val) +{ + if (VAL(val) == opt->min || VAL(val) == opt->max) + return 0; + VAL(val) = opt->min; + return M_OPT_OUT_OF_RANGE; +} + +static int parse_flag(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len) { + if (!bstrcmp0(param, "yes")) { + if (dst) + VAL(dst) = opt->max; + return 1; + } + if (!bstrcmp0(param, "no")) { + if (dst) + VAL(dst) = opt->min; + return 1; + } + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid parameter for %.*s flag: %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } else { + if (dst) + VAL(dst) = opt->max; + return 0; + } +} + +static char *print_flag(const m_option_t *opt, const void *val) +{ + if (VAL(val) == opt->min) + return talloc_strdup(NULL, "no"); + else + return talloc_strdup(NULL, "yes"); +} + +static void add_flag(const m_option_t *opt, void *val, double add, bool wrap) +{ + if (fabs(add) < 0.5) + return; + bool state = VAL(val) != opt->min; + state = wrap ? !state : add > 0; + VAL(val) = state ? opt->max : opt->min; +} + +const m_option_type_t m_option_type_flag = { + // need yes or no in config files + .name = "Flag", + .size = sizeof(int), + .flags = M_OPT_TYPE_OPTIONAL_PARAM, + .parse = parse_flag, + .print = print_flag, + .copy = copy_opt, + .add = add_flag, + .clamp = clamp_flag, +}; + +// Single-value, write-only flag + +static int parse_store(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0 || bstrcmp0(param, "yes") == 0) { + if (dst) + VAL(dst) = opt->max; + return 0; + } else { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid parameter for %.*s flag: %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_DISALLOW_PARAM; + } +} + +const m_option_type_t m_option_type_store = { + // can only be activated + .name = "Flag", + .size = sizeof(int), + .flags = M_OPT_TYPE_OPTIONAL_PARAM, + .parse = parse_store, +}; + +// Same for float types + +#undef VAL +#define VAL(x) (*(float *)(x)) + +static int parse_store_float(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0 || bstrcmp0(param, "yes") == 0) { + if (dst) + VAL(dst) = opt->max; + return 0; + } else { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid parameter for %.*s flag: %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_DISALLOW_PARAM; + } +} + +const m_option_type_t m_option_type_float_store = { + // can only be activated + .name = "Flag", + .size = sizeof(float), + .flags = M_OPT_TYPE_OPTIONAL_PARAM, + .parse = parse_store_float, +}; + +// Integer + +#undef VAL + +static int clamp_longlong(const m_option_t *opt, void *val) +{ + long long v = *(long long *)val; + int r = 0; + if ((opt->flags & M_OPT_MAX) && (v > opt->max)) { + v = opt->max; + r = M_OPT_OUT_OF_RANGE; + } + if ((opt->flags & M_OPT_MIN) && (v < opt->min)) { + v = opt->min; + r = M_OPT_OUT_OF_RANGE; + } + *(long long *)val = v; + return r; +} + +static int parse_longlong(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + struct bstr rest; + long long tmp_int = bstrtoll(param, &rest, 10); + if (rest.len) + tmp_int = bstrtoll(param, &rest, 0); + if (rest.len) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be an integer: %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } + + if ((opt->flags & M_OPT_MIN) && (tmp_int < opt->min)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be >= %d: %.*s\n", + BSTR_P(name), (int) opt->min, BSTR_P(param)); + return M_OPT_OUT_OF_RANGE; + } + + if ((opt->flags & M_OPT_MAX) && (tmp_int > opt->max)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be <= %d: %.*s\n", + BSTR_P(name), (int) opt->max, BSTR_P(param)); + return M_OPT_OUT_OF_RANGE; + } + + if (dst) + *(long long *)dst = tmp_int; + + return 1; +} + +static int clamp_int(const m_option_t *opt, void *val) +{ + long long tmp = *(int *)val; + int r = clamp_longlong(opt, &tmp); + *(int *)val = tmp; + return r; +} + +static int clamp_int64(const m_option_t *opt, void *val) +{ + long long tmp = *(int64_t *)val; + int r = clamp_longlong(opt, &tmp); + *(int64_t *)val = tmp; + return r; +} + +static int parse_int(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + long long tmp; + int r = parse_longlong(opt, name, param, &tmp); + if (r >= 0 && dst) + *(int *)dst = tmp; + return r; +} + +static int parse_int64(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + long long tmp; + int r = parse_longlong(opt, name, param, &tmp); + if (r >= 0 && dst) + *(int64_t *)dst = tmp; + return r; +} + +static char *print_int(const m_option_t *opt, const void *val) +{ + if (opt->type->size == sizeof(int64_t)) + return talloc_asprintf(NULL, "%"PRId64, *(const int64_t *)val); + return talloc_asprintf(NULL, "%d", *(const int *)val); +} + +static void add_int64(const m_option_t *opt, void *val, double add, bool wrap) +{ + int64_t v = *(int64_t *)val; + + v = v + add; + + bool is64 = opt->type->size == sizeof(int64_t); + int64_t nmin = is64 ? INT64_MIN : INT_MIN; + int64_t nmax = is64 ? INT64_MAX : INT_MAX; + + int64_t min = (opt->flags & M_OPT_MIN) ? opt->min : nmin; + int64_t max = (opt->flags & M_OPT_MAX) ? opt->max : nmax; + + if (v < min) + v = wrap ? max : min; + if (v > max) + v = wrap ? min : max; + + *(int64_t *)val = v; +} + +static void add_int(const m_option_t *opt, void *val, double add, bool wrap) +{ + int64_t tmp = *(int *)val; + add_int64(opt, &tmp, add, wrap); + *(int *)val = tmp; +} + +const m_option_type_t m_option_type_int = { + .name = "Integer", + .size = sizeof(int), + .parse = parse_int, + .print = print_int, + .copy = copy_opt, + .add = add_int, + .clamp = clamp_int, +}; + +const m_option_type_t m_option_type_int64 = { + .name = "Integer64", + .size = sizeof(int64_t), + .parse = parse_int64, + .print = print_int, + .copy = copy_opt, + .add = add_int64, + .clamp = clamp_int64, +}; + +static int parse_intpair(const struct m_option *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + struct bstr s = param; + int end = -1; + int start = bstrtoll(s, &s, 10); + if (s.len == param.len) + goto bad; + if (s.len > 0) { + if (!bstr_startswith0(s, "-")) + goto bad; + s = bstr_cut(s, 1); + } + if (s.len > 0) + end = bstrtoll(s, &s, 10); + if (s.len > 0) + goto bad; + + if (dst) { + int *p = dst; + p[0] = start; + p[1] = end; + } + + return 1; + +bad: + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Invalid integer range " + "specification for option %.*s: %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; +} + +const struct m_option_type m_option_type_intpair = { + .name = "Int[-Int]", + .size = sizeof(int[2]), + .parse = parse_intpair, + .copy = copy_opt, +}; + +static int clamp_choice(const m_option_t *opt, void *val) +{ + int v = *(int *)val; + if ((opt->flags & M_OPT_MIN) && (opt->flags & M_OPT_MAX)) { + if (v >= opt->min && v <= opt->max) + return 0; + } + ; + for (struct m_opt_choice_alternatives *alt = opt->priv; alt->name; alt++) { + if (alt->value == v) + return 0; + } + return M_OPT_INVALID; +} + +static int parse_choice(const struct m_option *opt, struct bstr name, + struct bstr param, void *dst) +{ + struct m_opt_choice_alternatives *alt = opt->priv; + for ( ; alt->name; alt++) + if (!bstrcmp0(param, alt->name)) + break; + if (!alt->name) { + if (param.len == 0) + return M_OPT_MISSING_PARAM; + if ((opt->flags & M_OPT_MIN) && (opt->flags & M_OPT_MAX)) { + long long val; + if (parse_longlong(opt, name, param, &val) == 1) { + if (dst) + *(int *)dst = val; + return 1; + } + } + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid value for option %.*s: %.*s\n", + BSTR_P(name), BSTR_P(param)); + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Valid values are:"); + for (alt = opt->priv; alt->name; alt++) + mp_msg(MSGT_CFGPARSER, MSGL_ERR, " %s", alt->name); + if ((opt->flags & M_OPT_MIN) && (opt->flags & M_OPT_MAX)) + mp_msg(MSGT_CFGPARSER, MSGL_ERR, " %g-%g", opt->min, opt->max); + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "\n"); + return M_OPT_INVALID; + } + if (dst) + *(int *)dst = alt->value; + + return 1; +} + +static char *print_choice(const m_option_t *opt, const void *val) +{ + int v = *(int *)val; + struct m_opt_choice_alternatives *alt; + for (alt = opt->priv; alt->name; alt++) + if (alt->value == v) + return talloc_strdup(NULL, alt->name); + if ((opt->flags & M_OPT_MIN) && (opt->flags & M_OPT_MAX)) { + if (v >= opt->min && v <= opt->max) + return talloc_asprintf(NULL, "%d", v); + } + abort(); +} + +static void choice_get_min_max(const struct m_option *opt, int *min, int *max) +{ + assert(opt->type == &m_option_type_choice); + *min = INT_MAX; + *max = INT_MIN; + for (struct m_opt_choice_alternatives *alt = opt->priv; alt->name; alt++) { + *min = FFMIN(*min, alt->value); + *max = FFMAX(*max, alt->value); + } + if ((opt->flags & M_OPT_MIN) && (opt->flags & M_OPT_MAX)) { + *min = FFMIN(*min, opt->min); + *max = FFMAX(*max, opt->max); + } +} + +static void check_choice(int dir, int val, bool *found, int *best, int choice) +{ + if ((dir == -1 && (!(*found) || choice > (*best)) && choice < val) || + (dir == +1 && (!(*found) || choice < (*best)) && choice > val)) + { + *found = true; + *best = choice; + } +} + +static void add_choice(const m_option_t *opt, void *val, double add, bool wrap) +{ + assert(opt->type == &m_option_type_choice); + int dir = add > 0 ? +1 : -1; + bool found = false; + int ival = *(int *)val; + int best = 0; // init. value unused + + if (fabs(add) < 0.5) + return; + + if ((opt->flags & M_OPT_MIN) && (opt->flags & M_OPT_MAX)) { + int newval = ival + add; + if (ival >= opt->min && ival <= opt->max && + newval >= opt->min && newval <= opt->max) + { + found = true; + best = newval; + } else { + check_choice(dir, ival, &found, &best, opt->min); + check_choice(dir, ival, &found, &best, opt->max); + } + } + + for (struct m_opt_choice_alternatives *alt = opt->priv; alt->name; alt++) + check_choice(dir, ival, &found, &best, alt->value); + + if (!found) { + int min, max; + choice_get_min_max(opt, &min, &max); + best = (dir == -1) ^ wrap ? min : max; + } + + *(int *)val = best; +} + +const struct m_option_type m_option_type_choice = { + .name = "String", // same as arbitrary strings in option list for now + .size = sizeof(int), + .parse = parse_choice, + .print = print_choice, + .copy = copy_opt, + .add = add_choice, + .clamp = clamp_choice, +}; + +// Float + +#undef VAL +#define VAL(x) (*(double *)(x)) + +static int clamp_double(const m_option_t *opt, void *val) +{ + double v = VAL(val); + int r = 0; + if ((opt->flags & M_OPT_MAX) && (v > opt->max)) { + v = opt->max; + r = M_OPT_OUT_OF_RANGE; + } + if ((opt->flags & M_OPT_MIN) && (v < opt->min)) { + v = opt->min; + r = M_OPT_OUT_OF_RANGE; + } + if (!isfinite(v)) { + v = opt->min; + r = M_OPT_OUT_OF_RANGE; + } + VAL(val) = v; + return r; +} + +static int parse_double(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + struct bstr rest; + double tmp_float = bstrtod(param, &rest); + + if (bstr_eatstart0(&rest, ":") || bstr_eatstart0(&rest, "/")) + tmp_float /= bstrtod(rest, &rest); + + if (rest.len) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be a floating point number or a " + "ratio (numerator[:/]denominator): %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } + + if (opt->flags & M_OPT_MIN) + if (tmp_float < opt->min) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be >= %f: %.*s\n", + BSTR_P(name), opt->min, BSTR_P(param)); + return M_OPT_OUT_OF_RANGE; + } + + if (opt->flags & M_OPT_MAX) + if (tmp_float > opt->max) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be <= %f: %.*s\n", + BSTR_P(name), opt->max, BSTR_P(param)); + return M_OPT_OUT_OF_RANGE; + } + + if (!isfinite(tmp_float)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "The %.*s option must be a finite number: %.*s\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_OUT_OF_RANGE; + } + + if (dst) + VAL(dst) = tmp_float; + return 1; +} + +static char *print_double(const m_option_t *opt, const void *val) +{ + return talloc_asprintf(NULL, "%f", VAL(val)); +} + +static char *print_double_f3(const m_option_t *opt, const void *val) +{ + return talloc_asprintf(NULL, "%.3f", VAL(val)); +} + +static void add_double(const m_option_t *opt, void *val, double add, bool wrap) +{ + double v = VAL(val); + + v = v + add; + + double min = (opt->flags & M_OPT_MIN) ? opt->min : -INFINITY; + double max = (opt->flags & M_OPT_MAX) ? opt->max : +INFINITY; + + if (v < min) + v = wrap ? max : min; + if (v > max) + v = wrap ? min : max; + + VAL(val) = v; +} + +const m_option_type_t m_option_type_double = { + // double precision float or ratio (numerator[:/]denominator) + .name = "Double", + .size = sizeof(double), + .parse = parse_double, + .print = print_double, + .pretty_print = print_double_f3, + .copy = copy_opt, + .clamp = clamp_double, +}; + +#undef VAL +#define VAL(x) (*(float *)(x)) + +static int clamp_float(const m_option_t *opt, void *val) +{ + double tmp = VAL(val); + int r = clamp_double(opt, &tmp); + VAL(val) = tmp; + return r; +} + +static int parse_float(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + double tmp; + int r = parse_double(opt, name, param, &tmp); + if (r == 1 && dst) + VAL(dst) = tmp; + return r; +} + +static char *print_float(const m_option_t *opt, const void *val) +{ + return talloc_asprintf(NULL, "%f", VAL(val)); +} + +static char *print_float_f3(const m_option_t *opt, const void *val) +{ + return talloc_asprintf(NULL, "%.3f", VAL(val)); +} + +static void add_float(const m_option_t *opt, void *val, double add, bool wrap) +{ + double tmp = VAL(val); + add_double(opt, &tmp, add, wrap); + VAL(val) = tmp; +} + +const m_option_type_t m_option_type_float = { + // floating point number or ratio (numerator[:/]denominator) + .name = "Float", + .size = sizeof(float), + .parse = parse_float, + .print = print_float, + .pretty_print = print_float_f3, + .copy = copy_opt, + .add = add_float, + .clamp = clamp_float, +}; + +///////////// String + +#undef VAL +#define VAL(x) (*(char **)(x)) + +static char *unescape_string(void *talloc_ctx, bstr str) +{ + char *res = talloc_strdup(talloc_ctx, ""); + while (str.len) { + bstr rest; + bool esc = bstr_split_tok(str, "\\", &str, &rest); + res = talloc_strndup_append_buffer(res, str.start, str.len); + if (esc) { + if (!mp_parse_escape(&rest, &res)) { + talloc_free(res); + return NULL; + } + } + str = rest; + } + return res; +} + +static char *escape_string(char *str0) +{ + char *res = talloc_strdup(NULL, ""); + bstr str = bstr0(str0); + while (str.len) { + bstr rest; + bool esc = bstr_split_tok(str, "\\", &str, &rest); + res = talloc_strndup_append_buffer(res, str.start, str.len); + if (esc) + res = talloc_strdup_append_buffer(res, "\\\\"); + str = rest; + } + return res; +} + +static int clamp_str(const m_option_t *opt, void *val) +{ + char *v = VAL(val); + int len = v ? strlen(v) : 0; + if ((opt->flags & M_OPT_MIN) && (len < opt->min)) + return M_OPT_OUT_OF_RANGE; + if ((opt->flags & M_OPT_MAX) && (len > opt->max)) + return M_OPT_OUT_OF_RANGE; + return 0; +} + +static int parse_str(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + int r = 1; + void *tmp = talloc_new(NULL); + + if (param.start == NULL) { + r = M_OPT_MISSING_PARAM; + goto exit; + } + + m_opt_string_validate_fn validate = opt->priv; + if (validate) { + r = validate(opt, name, param); + if (r < 0) + goto exit; + } + + if (opt->flags & M_OPT_PARSE_ESCAPES) { + char *res = unescape_string(tmp, param); + if (!res) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Parameter has broken escapes: %.*s\n", BSTR_P(param)); + r = M_OPT_INVALID; + goto exit; + } + param = bstr0(res); + } + + if ((opt->flags & M_OPT_MIN) && (param.len < opt->min)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Parameter must be >= %d chars: %.*s\n", + (int) opt->min, BSTR_P(param)); + r = M_OPT_OUT_OF_RANGE; + goto exit; + } + + if ((opt->flags & M_OPT_MAX) && (param.len > opt->max)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Parameter must be <= %d chars: %.*s\n", + (int) opt->max, BSTR_P(param)); + r = M_OPT_OUT_OF_RANGE; + goto exit; + } + + if (dst) { + talloc_free(VAL(dst)); + VAL(dst) = bstrdup0(NULL, param); + } + +exit: + talloc_free(tmp); + return r; +} + +static char *print_str(const m_option_t *opt, const void *val) +{ + bool need_escape = opt->flags & M_OPT_PARSE_ESCAPES; + char *s = val ? VAL(val) : NULL; + return s ? (need_escape ? escape_string(s) : talloc_strdup(NULL, s)) : NULL; +} + +static void copy_str(const m_option_t *opt, void *dst, const void *src) +{ + if (dst && src) { + talloc_free(VAL(dst)); + VAL(dst) = talloc_strdup(NULL, VAL(src)); + } +} + +static void free_str(void *src) +{ + if (src && VAL(src)) { + talloc_free(VAL(src)); + VAL(src) = NULL; + } +} + +const m_option_type_t m_option_type_string = { + .name = "String", + .size = sizeof(char *), + .flags = M_OPT_TYPE_DYNAMIC, + .parse = parse_str, + .print = print_str, + .copy = copy_str, + .free = free_str, + .clamp = clamp_str, +}; + +//////////// String list + +#undef VAL +#define VAL(x) (*(char ***)(x)) + +#define OP_NONE 0 +#define OP_ADD 1 +#define OP_PRE 2 +#define OP_DEL 3 +#define OP_CLR 4 +#define OP_TOGGLE 5 + +static void free_str_list(void *dst) +{ + char **d; + int i; + + if (!dst || !VAL(dst)) + return; + d = VAL(dst); + + for (i = 0; d[i] != NULL; i++) + talloc_free(d[i]); + talloc_free(d); + VAL(dst) = NULL; +} + +static int str_list_add(char **add, int n, void *dst, int pre) +{ + if (!dst) + return M_OPT_PARSER_ERR; + char **lst = VAL(dst); + + int ln; + for (ln = 0; lst && lst[ln]; ln++) + /**/; + + lst = talloc_realloc(NULL, lst, char *, n + ln + 1); + + if (pre) { + memmove(&lst[n], lst, ln * sizeof(char *)); + memcpy(lst, add, n * sizeof(char *)); + } else + memcpy(&lst[ln], add, n * sizeof(char *)); + // (re-)add NULL-termination + lst[ln + n] = NULL; + + talloc_free(add); + + VAL(dst) = lst; + + return 1; +} + +static int str_list_del(char **del, int n, void *dst) +{ + char **lst, *ep; + int i, ln, s; + long idx; + + if (!dst) + return M_OPT_PARSER_ERR; + lst = VAL(dst); + + for (ln = 0; lst && lst[ln]; ln++) + /**/; + s = ln; + + for (i = 0; del[i] != NULL; i++) { + idx = strtol(del[i], &ep, 0); + if (*ep) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Invalid index: %s\n", del[i]); + talloc_free(del[i]); + continue; + } + talloc_free(del[i]); + if (idx < 0 || idx >= ln) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Index %ld is out of range.\n", idx); + continue; + } else if (!lst[idx]) + continue; + talloc_free(lst[idx]); + lst[idx] = NULL; + s--; + } + talloc_free(del); + + if (s == 0) { + talloc_free(lst); + VAL(dst) = NULL; + return 1; + } + + // Don't bother shrinking the list allocation + for (i = 0, n = 0; i < ln; i++) { + if (!lst[i]) + continue; + lst[n] = lst[i]; + n++; + } + lst[s] = NULL; + + return 1; +} + +static struct bstr get_nextsep(struct bstr *ptr, char sep, bool modify) +{ + struct bstr str = *ptr; + struct bstr orig = str; + for (;;) { + int idx = bstrchr(str, sep); + if (idx > 0 && str.start[idx - 1] == '\\') { + if (modify) { + memmove(str.start + idx - 1, str.start + idx, str.len - idx); + str.len--; + str = bstr_cut(str, idx); + } else + str = bstr_cut(str, idx + 1); + } else { + str = bstr_cut(str, idx < 0 ? str.len : idx); + break; + } + } + *ptr = str; + return bstr_splice(orig, 0, str.start - orig.start); +} + +static int parse_str_list(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + char **res; + int op = OP_NONE; + int len = strlen(opt->name); + if (opt->name[len - 1] == '*' && (name.len > len - 1)) { + struct bstr suffix = bstr_cut(name, len - 1); + if (bstrcmp0(suffix, "-add") == 0) + op = OP_ADD; + else if (bstrcmp0(suffix, "-pre") == 0) + op = OP_PRE; + else if (bstrcmp0(suffix, "-del") == 0) + op = OP_DEL; + else if (bstrcmp0(suffix, "-clr") == 0) + op = OP_CLR; + else + return M_OPT_UNKNOWN; + } + + // Clear the list ?? + if (op == OP_CLR) { + if (dst) + free_str_list(dst); + return 0; + } + + // All other ops need a param + if (param.len == 0 && op != OP_NONE) + return M_OPT_MISSING_PARAM; + + // custom type for "profile" calls this but uses ->priv for something else + char separator = opt->type == &m_option_type_string_list && opt->priv ? + *(char *)opt->priv : OPTION_LIST_SEPARATOR; + int n = 0; + struct bstr str = param; + while (str.len) { + get_nextsep(&str, separator, 0); + str = bstr_cut(str, 1); + n++; + } + if (n == 0 && op != OP_NONE) + return M_OPT_INVALID; + if (((opt->flags & M_OPT_MIN) && (n < opt->min)) || + ((opt->flags & M_OPT_MAX) && (n > opt->max))) + return M_OPT_OUT_OF_RANGE; + + if (!dst) + return 1; + + res = talloc_array(NULL, char *, n + 2); + str = bstrdup(NULL, param); + char *ptr = str.start; + n = 0; + + while (1) { + struct bstr el = get_nextsep(&str, separator, 1); + res[n] = bstrdup0(NULL, el); + n++; + if (!str.len) + break; + str = bstr_cut(str, 1); + } + res[n] = NULL; + talloc_free(ptr); + + switch (op) { + case OP_ADD: + return str_list_add(res, n, dst, 0); + case OP_PRE: + return str_list_add(res, n, dst, 1); + case OP_DEL: + return str_list_del(res, n, dst); + } + + if (VAL(dst)) + free_str_list(dst); + VAL(dst) = res; + + if (!res[0]) + free_str_list(dst); + + return 1; +} + +static void copy_str_list(const m_option_t *opt, void *dst, const void *src) +{ + int n; + char **d, **s; + + if (!(dst && src)) + return; + s = VAL(src); + + if (VAL(dst)) + free_str_list(dst); + + if (!s) { + VAL(dst) = NULL; + return; + } + + for (n = 0; s[n] != NULL; n++) + /* NOTHING */; + d = talloc_array(NULL, char *, n + 1); + for (; n >= 0; n--) + d[n] = talloc_strdup(NULL, s[n]); + + VAL(dst) = d; +} + +static char *print_str_list(const m_option_t *opt, const void *src) +{ + char **lst = NULL; + char *ret = NULL; + + if (!(src && VAL(src))) + return NULL; + lst = VAL(src); + + for (int i = 0; lst[i]; i++) { + if (ret) + ret = talloc_strdup_append_buffer(ret, ","); + ret = talloc_strdup_append_buffer(ret, lst[i]); + } + return ret; +} + +const m_option_type_t m_option_type_string_list = { + /* A list of strings separated by ','. + * Option with a name ending in '*' permits using the following suffixes: + * -add: Add the given parameters at the end of the list. + * -pre: Add the given parameters at the beginning of the list. + * -del: Remove the entry at the given indices. + * -clr: Clear the list. + * e.g: -vf-add flip,mirror -vf-del 2,5 + */ + .name = "String list", + .size = sizeof(char **), + .flags = M_OPT_TYPE_DYNAMIC | M_OPT_TYPE_ALLOW_WILDCARD, + .parse = parse_str_list, + .print = print_str_list, + .copy = copy_str_list, + .free = free_str_list, +}; + + +/////////////////// Print + +static int parse_print(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (opt->type == CONF_TYPE_PRINT) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "%s", mp_gtext(opt->p)); + } else { + char *name0 = bstrdup0(NULL, name); + char *param0 = bstrdup0(NULL, param); + int r = ((m_opt_func_full_t) opt->p)(opt, name0, param0); + talloc_free(name0); + talloc_free(param0); + return r; + } + + if (opt->priv == NULL) + return M_OPT_EXIT; + return 0; +} + +const m_option_type_t m_option_type_print = { + .name = "Print", + .flags = M_OPT_TYPE_OPTIONAL_PARAM, + .parse = parse_print, +}; + +const m_option_type_t m_option_type_print_func_param = { + .name = "Print", + .flags = M_OPT_TYPE_ALLOW_WILDCARD, + .parse = parse_print, +}; + +const m_option_type_t m_option_type_print_func = { + .name = "Print", + .flags = M_OPT_TYPE_ALLOW_WILDCARD | M_OPT_TYPE_OPTIONAL_PARAM, + .parse = parse_print, +}; + + +/////////////////////// Subconfig +#undef VAL +#define VAL(x) (*(char ***)(x)) + +// Read s sub-option name, or a positional sub-opt value. +// Return 0 on succes, M_OPT_ error code otherwise. +// optname is for error reporting. +static int read_subparam(bstr optname, bstr *str, bstr *out_subparam) +{ + bstr p = *str; + bstr subparam = {0}; + + if (bstr_eatstart0(&p, "\"")) { + int optlen = bstrcspn(p, "\""); + subparam = bstr_splice(p, 0, optlen); + p = bstr_cut(p, optlen); + if (!bstr_startswith0(p, "\"")) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Terminating '\"' missing for '%.*s'\n", + BSTR_P(optname)); + return M_OPT_INVALID; + } + p = bstr_cut(p, 1); + } else if (bstr_eatstart0(&p, "[")) { + if (!bstr_split_tok(p, "]", &subparam, &p)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Terminating ']' missing for '%.*s'\n", + BSTR_P(optname)); + return M_OPT_INVALID; + } + } else if (bstr_eatstart0(&p, "%")) { + int optlen = bstrtoll(p, &p, 0); + if (!bstr_startswith0(p, "%") || (optlen > p.len - 1)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid length %d for '%.*s'\n", + optlen, BSTR_P(optname)); + return M_OPT_INVALID; + } + subparam = bstr_splice(p, 1, optlen + 1); + p = bstr_cut(p, optlen + 1); + } else { + // Skip until the next character that could possibly be a meta + // character in option parsing. + int optlen = bstrcspn(p, ":=,\\%\"'[]"); + subparam = bstr_splice(p, 0, optlen); + p = bstr_cut(p, optlen); + } + + *str = p; + *out_subparam = subparam; + return 0; +} + +// Return 0 on success, otherwise error code +// On success, set *out_name and *out_val, and advance *str +// out_val.start is NULL if there was no parameter. +// optname is for error reporting. +static int split_subconf(bstr optname, bstr *str, bstr *out_name, bstr *out_val) +{ + bstr p = *str; + bstr subparam = {0}; + bstr subopt; + int r = read_subparam(optname, &p, &subopt); + if (r < 0) + return r; + if (bstr_eatstart0(&p, "=")) { + r = read_subparam(subopt, &p, &subparam); + if (r < 0) + return r; + } + *str = p; + *out_name = subopt; + *out_val = subparam; + return 0; +} + +static int parse_subconf(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + int nr = 0; + char **lst = NULL; + + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + struct bstr p = param; + + while (p.len) { + bstr subopt, subparam; + int r = split_subconf(name, &p, &subopt, &subparam); + if (r < 0) + return r; + if (bstr_startswith0(p, ":")) + p = bstr_cut(p, 1); + else if (p.len > 0) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Incorrect termination for '%.*s'\n", BSTR_P(subopt)); + return M_OPT_INVALID; + } + + if (dst) { + lst = talloc_realloc(NULL, lst, char *, 2 * (nr + 2)); + lst[2 * nr] = bstrto0(lst, subopt); + lst[2 * nr + 1] = bstrto0(lst, subparam); + memset(&lst[2 * (nr + 1)], 0, 2 * sizeof(char *)); + nr++; + } + } + + if (dst) + VAL(dst) = lst; + + return 1; +} + +const m_option_type_t m_option_type_subconfig = { + // The syntax is -option opt1=foo:flag:opt2=blah + .name = "Subconfig", + .flags = M_OPT_TYPE_HAS_CHILD, + .parse = parse_subconf, +}; + +const m_option_type_t m_option_type_subconfig_struct = { + .name = "Subconfig", + .flags = M_OPT_TYPE_HAS_CHILD | M_OPT_TYPE_USE_SUBSTRUCT, + .parse = parse_subconf, +}; + +static int parse_color(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + bstr val = param; + struct m_color color = {0}; + + if (bstr_eatstart0(&val, "#")) { + // #[AA]RRGGBB + if (val.len != 6 && val.len != 8) + goto error; + bool has_alpha = val.len == 8; + uint32_t c = bstrtoll(val, &val, 16); + if (val.len) + goto error; + color = (struct m_color) { + (c >> 16) & 0xFF, + (c >> 8) & 0xFF, + c & 0xFF, + has_alpha ? (c >> 24) & 0xFF : 0xFF, + }; + } else { + goto error; + } + + if (dst) + *((struct m_color *)dst) = color; + + return 1; + +error: + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: invalid color: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Valid colors must be in the form #RRRGGBB or #AARRGGBB (in hex)\n"); + return M_OPT_INVALID; +} + +const m_option_type_t m_option_type_color = { + .name = "Color", + .size = sizeof(struct m_color), + .parse = parse_color, +}; + + +// Parse a >=0 number starting at s. Set s to the string following the number. +// If the number ends with '%', eat that and set *out_per to true, but only +// if the number is between 0-100; if not, don't eat anything, even the number. +static bool eat_num_per(bstr *s, int *out_num, bool *out_per) +{ + bstr rest; + long long v = bstrtoll(*s, &rest, 10); + if (s->len == rest.len || v < INT_MIN || v > INT_MAX) + return false; + *out_num = v; + *out_per = false; + *s = rest; + if (bstr_eatstart0(&rest, "%") && v >= 0 && v <= 100) { + *out_per = true; + *s = rest; + } + return true; +} + +static bool parse_geometry_str(struct m_geometry *gm, bstr s) +{ + *gm = (struct m_geometry) { .x = INT_MIN, .y = INT_MIN }; + if (s.len == 0) + return true; + // Approximate grammar: + // [W[xH]][{+-}X{+-}Y] | [X:Y] + // (meaning: [optional] {one character of} one|alternative) + // Every number can be followed by '%' + int num; + bool per; + +#define READ_NUM(F, F_PER) do { \ + if (!eat_num_per(&s, &num, &per)) \ + goto error; \ + gm->F = num; \ + gm->F_PER = per; \ +} while(0) + +#define READ_SIGN(F) do { \ + if (bstr_eatstart0(&s, "+")) { \ + gm->F = false; \ + } else if (bstr_eatstart0(&s, "-")) {\ + gm->F = true; \ + } else goto error; \ +} while(0) + + if (bstrchr(s, ':') < 0) { + gm->wh_valid = true; + if (!bstr_startswith0(s, "+") && !bstr_startswith0(s, "-")) { + READ_NUM(w, w_per); + if (bstr_eatstart0(&s, "x")) + READ_NUM(h, h_per); + } + if (s.len > 0) { + gm->xy_valid = true; + READ_SIGN(x_sign); + READ_NUM(x, x_per); + READ_SIGN(y_sign); + READ_NUM(y, y_per); + } + } else { + gm->xy_valid = true; + READ_NUM(x, x_per); + if (!bstr_eatstart0(&s, ":")) + goto error; + READ_NUM(y, y_per); + } + + return s.len == 0; + +error: + return false; +} + +#undef READ_NUM +#undef READ_SIGN + +// xpos,ypos: position of the left upper corner +// widw,widh: width and height of the window +// scrw,scrh: width and height of the current screen +// The input parameters should be set to a centered window (default fallbacks). +void m_geometry_apply(int *xpos, int *ypos, int *widw, int *widh, + int scrw, int scrh, struct m_geometry *gm) +{ + if (gm->wh_valid) { + int prew = *widw, preh = *widh; + if (gm->w > 0) + *widw = gm->w_per ? scrw * (gm->w / 100.0) : gm->w; + if (gm->h > 0) + *widh = gm->h_per ? scrh * (gm->h / 100.0) : gm->h; + // keep aspect if the other value is not set + double asp = (double)prew / preh; + if (gm->w > 0 && !(gm->h > 0)) { + *widh = *widw / asp; + } else if (!(gm->w > 0) && gm->h > 0) { + *widw = *widh * asp; + } + } + + if (gm->xy_valid) { + if (gm->x != INT_MIN) { + *xpos = gm->x; + if (gm->x_per) + *xpos = (scrw - *widw) * (*xpos / 100.0); + if (gm->x_sign) + *xpos = scrw - *widw - *xpos; + } + if (gm->y != INT_MIN) { + *ypos = gm->y; + if (gm->y_per) + *ypos = (scrh - *widh) * (*ypos / 100.0); + if (gm->y_sign) + *ypos = scrh - *widh - *ypos; + } + } +} + +static int parse_geometry(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + struct m_geometry gm; + if (!parse_geometry_str(&gm, param)) + goto error; + + if (dst) + *((struct m_geometry *)dst) = gm; + + return 1; + +error: + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: invalid geometry: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Valid format: [W[%%][xH[%%]]][{+-}X[%%]{+-}Y[%%]] | [X[%%]:Y[%%]]\n"); + return M_OPT_INVALID; +} + +const m_option_type_t m_option_type_geometry = { + .name = "Window geometry", + .size = sizeof(struct m_geometry), + .parse = parse_geometry, +}; + +static int parse_size_box(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + struct m_geometry gm; + if (!parse_geometry_str(&gm, param)) + goto error; + + if (gm.xy_valid) + goto error; + + if (dst) + *((struct m_geometry *)dst) = gm; + + return 1; + +error: + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: invalid size: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Valid format: W[%%][xH[%%]] or empty string\n"); + return M_OPT_INVALID; +} + +const m_option_type_t m_option_type_size_box = { + .name = "Window size", + .size = sizeof(struct m_geometry), + .parse = parse_size_box, +}; + + +#include "video/img_format.h" + +static int parse_imgfmt(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + if (!bstrcmp0(param, "help")) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "Available formats:"); + for (int i = 0; mp_imgfmt_list[i].name; i++) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " %s", mp_imgfmt_list[i].name); + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\n"); + return M_OPT_EXIT - 1; + } + + unsigned int fmt = mp_imgfmt_from_name(param, false); + if (!fmt) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: unknown format name: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } + + if (dst) + *((uint32_t *)dst) = fmt; + + return 1; +} + +const m_option_type_t m_option_type_imgfmt = { + // Please report any missing colorspaces + .name = "Image format", + .size = sizeof(uint32_t), + .parse = parse_imgfmt, + .copy = copy_opt, +}; + +static int parse_fourcc(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + unsigned int value; + + if (param.len == 4) { + uint8_t *s = param.start; + value = s[0] | (s[1] << 8) | (s[2] << 16) | (s[3] << 24); + } else { + bstr rest; + value = bstrtoll(param, &rest, 16); + if (rest.len != 0) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: invalid FourCC: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } + } + + if (dst) + *((unsigned int *)dst) = value; + + return 1; +} + +const m_option_type_t m_option_type_fourcc = { + .name = "FourCC", + .size = sizeof(unsigned int), + .parse = parse_fourcc, + .copy = copy_opt, +}; + +#include "audio/format.h" + +static int parse_afmt(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + if (!bstrcmp0(param, "help")) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "Available formats:"); + for (int i = 0; af_fmtstr_table[i].name; i++) + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " %s", af_fmtstr_table[i].name); + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\n"); + return M_OPT_EXIT - 1; + } + + int fmt = af_str2fmt_short(param); + if (fmt == -1) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: unknown format name: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } + + if (dst) + *((uint32_t *)dst) = fmt; + + return 1; +} + +const m_option_type_t m_option_type_afmt = { + // Please report any missing formats + .name = "Audio format", + .size = sizeof(uint32_t), + .parse = parse_afmt, + .copy = copy_opt, +}; + +#include "audio/chmap.h" + +static int parse_chmap(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + // min>0: at least min channels, min=0: empty ok, min=-1: invalid ok + int min_ch = (opt->flags & M_OPT_MIN) ? opt->min : 1; + + if (bstr_equals0(param, "help")) { + mp_chmap_print_help(MSGT_CFGPARSER, MSGL_INFO); + return M_OPT_EXIT - 1; + } + + if (param.len == 0 && min_ch >= 1) + return M_OPT_MISSING_PARAM; + + struct mp_chmap res = {0}; + if (!mp_chmap_from_str(&res, param)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Error parsing channel layout: %.*s\n", BSTR_P(param)); + return M_OPT_INVALID; + } + + if ((min_ch > 0 && !mp_chmap_is_valid(&res)) || + (min_ch >= 0 && mp_chmap_is_empty(&res))) + { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid channel layout: %.*s\n", BSTR_P(param)); + return M_OPT_INVALID; + } + + if (dst) + *(struct mp_chmap *)dst = res; + + return 1; +} + +const m_option_type_t m_option_type_chmap = { + .name = "Audio channels or channel map", + .size = sizeof(struct mp_chmap *), + .parse = parse_chmap, + .copy = copy_opt, +}; + +static int parse_timestring(struct bstr str, double *time, char endchar) +{ + int a, b, len; + double d; + *time = 0; /* ensure initialization for error cases */ + if (bstr_sscanf(str, "%d:%d:%lf%n", &a, &b, &d, &len) >= 3) + *time = 3600 * a + 60 * b + d; + else if (bstr_sscanf(str, "%d:%lf%n", &a, &d, &len) >= 2) + *time = 60 * a + d; + else if (bstr_sscanf(str, "%lf%n", &d, &len) >= 1) + *time = d; + else + return 0; /* unsupported time format */ + if (len < str.len && str.start[len] != endchar) + return 0; /* invalid extra characters at the end */ + if (!isfinite(*time)) + return 0; + return len; +} + + +static int parse_time(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + double time; + + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + if (!parse_timestring(param, &time, 0)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: invalid time: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + } + + if (dst) + *(double *)dst = time; + return 1; +} + +static char *pretty_print_time(const m_option_t *opt, const void *val) +{ + return mp_format_time(*(double *)val, false); +} + +const m_option_type_t m_option_type_time = { + .name = "Time", + .size = sizeof(double), + .parse = parse_time, + .print = print_double, + .pretty_print = pretty_print_time, + .copy = copy_opt, + .add = add_double, + .clamp = clamp_double, +}; + + +// Relative time + +static int parse_rel_time(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + struct m_rel_time t = {0}; + + if (param.len == 0) + return M_OPT_MISSING_PARAM; + + // Percent pos + if (bstr_endswith0(param, "%")) { + double percent = bstrtod(bstr_splice(param, 0, -1), ¶m); + if (param.len == 0 && percent >= 0 && percent <= 100) { + t.type = REL_TIME_PERCENT; + t.pos = percent; + goto out; + } + } + + // Chapter pos + if (bstr_startswith0(param, "#")) { + int chapter = bstrtoll(bstr_cut(param, 1), ¶m, 10); + if (param.len == 0 && chapter >= 1) { + t.type = REL_TIME_CHAPTER; + t.pos = chapter - 1; + goto out; + } + } + + bool sign = bstr_eatstart0(¶m, "-"); + double time; + if (parse_timestring(param, &time, 0)) { + t.type = sign ? REL_TIME_NEGATIVE : REL_TIME_ABSOLUTE; + t.pos = time; + goto out; + } + + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: invalid time or position: '%.*s'\n", + BSTR_P(name), BSTR_P(param)); + return M_OPT_INVALID; + +out: + if (dst) + *(struct m_rel_time *)dst = t; + return 1; +} + +const m_option_type_t m_option_type_rel_time = { + .name = "Relative time or percent position", + .size = sizeof(struct m_rel_time), + .parse = parse_rel_time, + .copy = copy_opt, +}; + + +//// Objects (i.e. filters, etc) settings + +#undef VAL +#define VAL(x) (*(m_obj_settings_t **)(x)) + +bool m_obj_list_find(struct m_obj_desc *dst, const struct m_obj_list *l, + bstr name) +{ + for (int i = 0; ; i++) { + if (!l->get_desc(dst, i)) + break; + if (bstr_equals0(name, dst->name)) + return true; + } + if (l->aliases) { + for (int i = 0; l->aliases[i][0]; i++) { + const char *aname = l->aliases[i][0]; + const char *alias = l->aliases[i][1]; + const char *opts = l->aliases[i][2]; + if (bstr_equals0(name, aname) && + m_obj_list_find(dst, l, bstr0(alias))) + { + if (opts) { + dst->init_options = opts; + } else { + // Assume it's deprecated in this case. + // Also, it's used by the VO code only, so whatever. + mp_msg(MSGT_CFGPARSER, MSGL_WARN, + "VO driver '%s' has been replaced with '%s'!\n", + aname, alias); + } + return true; + } + } + } + return false; +} + +static void obj_setting_free(m_obj_settings_t *item) +{ + talloc_free(item->name); + talloc_free(item->label); + free_str_list(&(item->attribs)); +} + +// If at least one item has a label, compare labels only - otherwise ignore them. +static bool obj_setting_equals(m_obj_settings_t *a, m_obj_settings_t *b) +{ + bstr la = bstr0(a->label), lb = bstr0(b->label); + if (la.len || lb.len) + return bstr_equals(la, lb); + if (strcmp(a->name, b->name) != 0) + return false; + + int a_attr_count = 0; + while (a->attribs && a->attribs[a_attr_count]) + a_attr_count++; + int b_attr_count = 0; + while (b->attribs && b->attribs[b_attr_count]) + b_attr_count++; + if (a_attr_count != b_attr_count) + return false; + for (int n = 0; n < a_attr_count; n++) { + if (strcmp(a->attribs[n], b->attribs[n]) != 0) + return false; + } + return true; +} + +static int obj_settings_list_num_items(m_obj_settings_t *obj_list) +{ + int num = 0; + while (obj_list && obj_list[num].name) + num++; + return num; +} + +static void obj_settings_list_del_at(m_obj_settings_t **p_obj_list, int idx) +{ + m_obj_settings_t *obj_list = *p_obj_list; + int num = obj_settings_list_num_items(obj_list); + + assert(idx >= 0 && idx < num); + + obj_setting_free(&obj_list[idx]); + + // Note: the NULL-terminating element is moved down as part of this + memmove(&obj_list[idx], &obj_list[idx + 1], + sizeof(m_obj_settings_t) * (num - idx)); + + *p_obj_list = talloc_realloc(NULL, obj_list, struct m_obj_settings, num); +} + +// Insert such that *p_obj_list[idx] is set to item. +// If idx < 0, set idx = count + idx + 1 (i.e. -1 inserts it as last element). +// Memory referenced by *item is not copied. +static void obj_settings_list_insert_at(m_obj_settings_t **p_obj_list, int idx, + m_obj_settings_t *item) +{ + int num = obj_settings_list_num_items(*p_obj_list); + if (idx < 0) + idx = num + idx + 1; + assert(idx >= 0 && idx <= num); + *p_obj_list = talloc_realloc(NULL, *p_obj_list, struct m_obj_settings, + num + 2); + memmove(*p_obj_list + idx + 1, *p_obj_list + idx, + (num - idx) * sizeof(m_obj_settings_t)); + (*p_obj_list)[idx] = *item; + (*p_obj_list)[num + 1] = (m_obj_settings_t){0}; +} + +static int obj_settings_list_find_by_label(m_obj_settings_t *obj_list, + bstr label) +{ + for (int n = 0; obj_list && obj_list[n].name; n++) { + if (label.len && bstr_equals0(label, obj_list[n].label)) + return n; + } + return -1; +} + +static int obj_settings_list_find_by_label0(m_obj_settings_t *obj_list, + const char *label) +{ + return obj_settings_list_find_by_label(obj_list, bstr0(label)); +} + +static int obj_settings_find_by_content(m_obj_settings_t *obj_list, + m_obj_settings_t *item) +{ + for (int n = 0; obj_list && obj_list[n].name; n++) { + if (obj_setting_equals(&obj_list[n], item)) + return n; + } + return -1; +} + +static void free_obj_settings_list(void *dst) +{ + int n; + m_obj_settings_t *d; + + if (!dst || !VAL(dst)) + return; + + d = VAL(dst); + for (n = 0; d[n].name; n++) + obj_setting_free(&d[n]); + talloc_free(d); + VAL(dst) = NULL; +} + +static void copy_obj_settings_list(const m_option_t *opt, void *dst, + const void *src) +{ + m_obj_settings_t *d, *s; + int n; + + if (!(dst && src)) + return; + + s = VAL(src); + + if (VAL(dst)) + free_obj_settings_list(dst); + if (!s) + return; + + for (n = 0; s[n].name; n++) + /* NOP */; + d = talloc_array(NULL, struct m_obj_settings, n + 1); + for (n = 0; s[n].name; n++) { + d[n].name = talloc_strdup(NULL, s[n].name); + d[n].label = talloc_strdup(NULL, s[n].label); + d[n].attribs = NULL; + copy_str_list(NULL, &(d[n].attribs), &(s[n].attribs)); + } + d[n].name = NULL; + d[n].label = NULL; + d[n].attribs = NULL; + VAL(dst) = d; +} + +// Consider -vf a=b=c:d=e. This verifies "b"="c" and "d"="e" and that the +// option names/values are correct. Try to determine whether an option +// without '=' sets a flag, or whether it's a positional argument. +static int get_obj_param(bstr opt_name, bstr obj_name, struct m_config *config, + bstr name, bstr val, int flags, int *nold, + bstr *out_name, bstr *out_val) +{ + int r; + + if (!config) + return 0; // skip + + // va.start != NULL => of the form name=val (not positional) + // If it's just "name", and the associated option exists and is a flag, + // don't accept it as positional argument. + if (val.start || m_config_option_requires_param(config, name) == 0) { + r = m_config_set_option_ext(config, name, val, flags); + if (r < 0) { + if (r == M_OPT_UNKNOWN) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: %.*s doesn't have a %.*s parameter.\n", + BSTR_P(opt_name), BSTR_P(obj_name), BSTR_P(name)); + return M_OPT_UNKNOWN; + } + if (r > M_OPT_EXIT) + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: " + "Error while parsing %.*s parameter %.*s (%.*s)\n", + BSTR_P(opt_name), BSTR_P(obj_name), BSTR_P(name), + BSTR_P(val)); + return r; + } + *out_name = name; + *out_val = val; + return 1; + } else { + val = name; + // positional fields + if (val.len == 0) { // Empty field, count it and go on + (*nold)++; + return 0; + } + const char *opt = m_config_get_positional_option(config, *nold); + if (!opt) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: %.*s has only %d " + "params, so you can't give more than %d unnamed params.\n", + BSTR_P(opt_name), BSTR_P(obj_name), *nold, *nold); + return M_OPT_OUT_OF_RANGE; + } + r = m_config_set_option_ext(config, bstr0(opt), val, flags); + if (r < 0) { + if (r > M_OPT_EXIT) + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: " + "Error while parsing %.*s parameter %s (%.*s)\n", + BSTR_P(opt_name), BSTR_P(obj_name), opt, BSTR_P(val)); + return r; + } + *out_name = bstr0(opt); + *out_val = val; + (*nold)++; + return 1; + } +} + +// Consider -vf a=b:c:d. This parses "b:c:d" into name/value pairs, stored as +// linear array in *_ret. In particular, config contains what options a the +// object takes, and verifies the option values as well. +// If config is NULL, all parameters are accepted without checking. +// _ret set to NULL can be used for checking-only. +// flags can contain any M_SETOPT_* flag. +int m_obj_parse_sub_config(struct bstr opt_name, struct bstr name, + struct bstr *pstr, struct m_config *config, + int flags, char ***ret) +{ + int nold = 0; + char **args = NULL; + int num_args = 0; + int r = 1; + + if (ret) { + args = *ret; + while (args && args[num_args]) + num_args++; + } + + while (pstr->len > 0) { + bstr fname, fval; + r = split_subconf(opt_name, pstr, &fname, &fval); + if (r < 0) + goto exit; + if (bstr_equals0(fname, "help")) + goto print_help; + r = get_obj_param(opt_name, name, config, fname, fval, flags, &nold, + &fname, &fval); + if (r < 0) + goto exit; + + if (r > 0 && ret) { + MP_TARRAY_APPEND(NULL, args, num_args, bstrto0(NULL, fname)); + MP_TARRAY_APPEND(NULL, args, num_args, bstrto0(NULL, fval)); + MP_TARRAY_APPEND(NULL, args, num_args, NULL); + MP_TARRAY_APPEND(NULL, args, num_args, NULL); + num_args -= 2; + } + + if (!bstr_eatstart0(pstr, ":")) + break; + } + + if (ret) { + if (num_args > 0) { + *ret = args; + args = NULL; + } else { + *ret = NULL; + } + } + + goto exit; + +print_help: ; + if (config) { + m_config_print_option_list(config); + } else { + mp_msg(MSGT_CFGPARSER, MSGL_WARN, "Option %.*s doesn't exist.\n", + BSTR_P(opt_name)); + } + r = M_OPT_EXIT - 1; + +exit: + free_str_list(&args); + return r; +} + +// Characters which may appear in a filter name +#define NAMECH "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" + +// Parse one item, e.g. -vf a=b:c:d,e=f:g => parse a=b:c:d into "a" and "b:c:d" +static int parse_obj_settings(struct bstr opt, struct bstr *pstr, + const struct m_obj_list *list, + m_obj_settings_t **_ret) +{ + int r; + char **plist = NULL; + struct m_obj_desc desc; + bstr label = {0}; + + if (bstr_eatstart0(pstr, "@")) { + if (!bstr_split_tok(*pstr, ":", &label, pstr)) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: ':' expected after label.\n", BSTR_P(opt)); + return M_OPT_INVALID; + } + } + + bool has_param = false; + int idx = bstrspn(*pstr, NAMECH); + bstr str = bstr_splice(*pstr, 0, idx); + *pstr = bstr_cut(*pstr, idx); + // video filters use "=", VOs use ":" + if (bstr_eatstart0(pstr, "=") || bstr_eatstart0(pstr, ":")) + has_param = true; + + bool legacy = false; + bool skip = false; + if (m_obj_list_find(&desc, list, str)) { + legacy = !desc.priv_size && list->legacy_hacks; + } else { + if (!list->allow_unknown_entries) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "Option %.*s: %.*s doesn't exist.\n", + BSTR_P(opt), BSTR_P(str)); + return M_OPT_INVALID; + } + desc = (struct m_obj_desc){0}; + skip = true; + } + + if (has_param) { + if (legacy) { + // Should perhaps be parsed as escape-able string. But this is a + // compatibility path, so it's not worth the trouble. + int next = bstrcspn(*pstr, ","); + bstr param = bstr_splice(*pstr, 0, next); + *pstr = bstr_cut(*pstr, next); + if (!bstrcmp0(param, "help")) { + mp_msg(MSGT_CFGPARSER, MSGL_WARN, + "Option %.*s: %.*s has no option description.\n", + BSTR_P(opt), BSTR_P(str)); + return M_OPT_EXIT - 1; + } + if (_ret) { + plist = talloc_zero_array(NULL, char *, 4); + plist[0] = talloc_strdup(NULL, "_oldargs_"); + plist[1] = bstrto0(NULL, param); + } + } else { + struct m_config *config = NULL; + if (!skip) + config = m_config_from_obj_desc(NULL, &desc); + r = m_obj_parse_sub_config(opt, str, pstr, config, + M_SETOPT_CHECK_ONLY, + _ret ? &plist : NULL); + talloc_free(config); + if (r < 0) + return r; + } + } + if (!_ret) + return 1; + + m_obj_settings_t item = { + .name = bstrto0(NULL, str), + .label = bstrdup0(NULL, label), + .attribs = plist, + }; + obj_settings_list_insert_at(_ret, -1, &item); + return 1; +} + +// Parse a single entry for -vf-del (return 0 if not applicable) +// mark_del is bounded by the number of items in dst +static int parse_obj_settings_del(struct bstr opt_name, struct bstr *param, + void *dst, bool *mark_del) +{ + bstr s = *param; + if (bstr_eatstart0(&s, "@")) { + // '@name:' -> parse as normal filter entry + // '@name,' or '@name<end>' -> parse here + int idx = bstrspn(s, NAMECH); + bstr label = bstr_splice(s, 0, idx); + s = bstr_cut(s, idx); + if (bstr_startswith0(s, ":")) + return 0; + if (dst) { + int label_index = obj_settings_list_find_by_label(VAL(dst), label); + if (label_index >= 0) { + mark_del[label_index] = true; + } else { + mp_msg(MSGT_CFGPARSER, MSGL_WARN, + "Option %.*s: item label @%.*s not found.\n", + BSTR_P(opt_name), BSTR_P(label)); + } + } + *param = s; + return 1; + } + + bstr rest; + long long id = bstrtoll(s, &rest, 0); + if (rest.len == s.len) + return 0; + + if (dst) { + int num = obj_settings_list_num_items(VAL(dst)); + if (id < 0) + id = num + id; + + if (id >= 0 && id < num) { + mark_del[id] = true; + } else { + mp_msg(MSGT_CFGPARSER, MSGL_WARN, + "Option %.*s: Index %lld is out of range.\n", + BSTR_P(opt_name), id); + } + } + + *param = rest; + return 1; +} + +static int parse_obj_settings_list(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + int len = strlen(opt->name); + m_obj_settings_t *res = NULL; + int op = OP_NONE; + bool *mark_del = NULL; + int num_items = obj_settings_list_num_items(dst ? VAL(dst) : 0); + struct m_obj_list *ol = opt->priv; + + assert(opt->priv); + + if (opt->name[len - 1] == '*' && (name.len > len - 1)) { + struct bstr suffix = bstr_cut(name, len - 1); + if (bstrcmp0(suffix, "-add") == 0) + op = OP_ADD; + else if (bstrcmp0(suffix, "-set") == 0) + op = OP_NONE; + else if (bstrcmp0(suffix, "-pre") == 0) + op = OP_PRE; + else if (bstrcmp0(suffix, "-del") == 0) + op = OP_DEL; + else if (bstrcmp0(suffix, "-clr") == 0) + op = OP_CLR; + else if (bstrcmp0(suffix, "-toggle") == 0) + op = OP_TOGGLE; + else { + char prefix[len]; + strncpy(prefix, opt->name, len - 1); + prefix[len - 1] = '\0'; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Option %.*s: unknown postfix %.*s\n" + "Supported postfixes are:\n" + " %s-set\n" + " Overwrite the old list with the given list\n\n" + " %s-add\n" + " Append the given list to the current list\n\n" + " %s-pre\n" + " Prepend the given list to the current list\n\n" + " %s-del x,y,...\n" + " Remove the given elements. Take the list element index (starting from 0).\n" + " Negative index can be used (i.e. -1 is the last element).\n" + " Filter names work as well.\n\n" + " %s-clr\n" + " Clear the current list.\n", + BSTR_P(name), BSTR_P(suffix), prefix, prefix, prefix, prefix, prefix); + + return M_OPT_UNKNOWN; + } + } + + if (!bstrcmp0(param, "help")) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "Available %s:\n", ol->description); + for (int n = 0; ; n++) { + struct m_obj_desc desc; + if (!ol->get_desc(&desc, n)) + break; + if (!desc.hidden) { + mp_msg(MSGT_CFGPARSER, MSGL_INFO, " %-15s: %s\n", + desc.name, desc.description); + } + } + mp_msg(MSGT_CFGPARSER, MSGL_INFO, "\n"); + return M_OPT_EXIT - 1; + } + + if (op == OP_CLR) { + if (dst) + free_obj_settings_list(dst); + return 0; + } else if (op == OP_DEL) { + mark_del = talloc_zero_array(NULL, bool, num_items + 1); + } + + if (op != OP_NONE && param.len == 0) + return M_OPT_MISSING_PARAM; + + while (param.len > 0) { + int r = 0; + if (op == OP_DEL) + r = parse_obj_settings_del(name, ¶m, dst, mark_del); + if (r == 0) { + r = parse_obj_settings(name, ¶m, ol, dst ? &res : NULL); + } + if (r < 0) + return r; + if (param.len > 0) { + const char sep[2] = {OPTION_LIST_SEPARATOR, 0}; + if (!bstr_eatstart0(¶m, sep)) + return M_OPT_INVALID; + if (param.len == 0) { + if (!ol->allow_trailer) + return M_OPT_INVALID; + if (dst) { + m_obj_settings_t item = { + .name = talloc_strdup(NULL, ""), + }; + obj_settings_list_insert_at(&res, -1, &item); + } + } + } + } + + if (dst) { + m_obj_settings_t *list = VAL(dst); + if (op == OP_PRE) { + int prepend_counter = 0; + for (int n = 0; res && res[n].name; n++) { + int label = obj_settings_list_find_by_label0(list, res[n].label); + if (label < 0) { + obj_settings_list_insert_at(&list, prepend_counter, &res[n]); + prepend_counter++; + } else { + // Prefer replacement semantics, instead of actually + // prepending. + obj_setting_free(&list[label]); + list[label] = res[n]; + } + } + talloc_free(res); + } else if (op == OP_ADD) { + for (int n = 0; res && res[n].name; n++) { + int label = obj_settings_list_find_by_label0(list, res[n].label); + if (label < 0) { + obj_settings_list_insert_at(&list, -1, &res[n]); + } else { + // Prefer replacement semantics, instead of actually + // appending. + obj_setting_free(&list[label]); + list[label] = res[n]; + } + } + talloc_free(res); + } else if (op == OP_TOGGLE) { + for (int n = 0; res && res[n].name; n++) { + int found = obj_settings_find_by_content(list, &res[n]); + if (found < 0) { + obj_settings_list_insert_at(&list, -1, &res[n]); + } else { + obj_settings_list_del_at(&list, found); + obj_setting_free(&res[n]); + } + } + talloc_free(res); + } else if (op == OP_DEL) { + for (int n = num_items - 1; n >= 0; n--) { + if (mark_del[n]) + obj_settings_list_del_at(&list, n); + } + for (int n = 0; res && res[n].name; n++) { + int found = obj_settings_find_by_content(list, &res[n]); + if (found < 0) { + mp_msg(MSGT_CFGPARSER, MSGL_WARN, + "Option %.*s: Item not found\n", BSTR_P(name)); + } else { + obj_settings_list_del_at(&list, found); + } + } + free_obj_settings_list(&res); + } else { + assert(op == OP_NONE); + free_obj_settings_list(&list); + list = res; + } + VAL(dst) = list; + } + + talloc_free(mark_del); + return 1; +} + +const m_option_type_t m_option_type_obj_settings_list = { + .name = "Object settings list", + .size = sizeof(m_obj_settings_t *), + .flags = M_OPT_TYPE_DYNAMIC | M_OPT_TYPE_ALLOW_WILDCARD, + .parse = parse_obj_settings_list, + .copy = copy_obj_settings_list, + .free = free_obj_settings_list, +}; diff --git a/mpvcore/m_option.h b/mpvcore/m_option.h new file mode 100644 index 0000000000..89f3caa652 --- /dev/null +++ b/mpvcore/m_option.h @@ -0,0 +1,646 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_M_OPTION_H +#define MPLAYER_M_OPTION_H + +#include <string.h> +#include <stddef.h> +#include <stdbool.h> + +#include "config.h" +#include "core/bstr.h" +#include "audio/chmap.h" + +// m_option allows to parse, print and copy data of various types. + +typedef struct m_option_type m_option_type_t; +typedef struct m_option m_option_t; +struct m_config; + +///////////////////////////// Options types declarations //////////////////// + +// Simple types +extern const m_option_type_t m_option_type_flag; +extern const m_option_type_t m_option_type_store; +extern const m_option_type_t m_option_type_float_store; +extern const m_option_type_t m_option_type_int; +extern const m_option_type_t m_option_type_int64; +extern const m_option_type_t m_option_type_intpair; +extern const m_option_type_t m_option_type_float; +extern const m_option_type_t m_option_type_double; +extern const m_option_type_t m_option_type_string; +extern const m_option_type_t m_option_type_string_list; +extern const m_option_type_t m_option_type_time; +extern const m_option_type_t m_option_type_rel_time; +extern const m_option_type_t m_option_type_choice; + +extern const m_option_type_t m_option_type_print; +extern const m_option_type_t m_option_type_print_func; +extern const m_option_type_t m_option_type_print_func_param; +extern const m_option_type_t m_option_type_subconfig; +extern const m_option_type_t m_option_type_subconfig_struct; +extern const m_option_type_t m_option_type_imgfmt; +extern const m_option_type_t m_option_type_fourcc; +extern const m_option_type_t m_option_type_afmt; +extern const m_option_type_t m_option_type_color; +extern const m_option_type_t m_option_type_geometry; +extern const m_option_type_t m_option_type_size_box; +extern const m_option_type_t m_option_type_chmap; + +// Callback used by m_option_type_print_func options. +typedef int (*m_opt_func_full_t)(const m_option_t *, const char *, const char *); + +enum m_rel_time_type { + REL_TIME_NONE, + REL_TIME_ABSOLUTE, + REL_TIME_NEGATIVE, + REL_TIME_PERCENT, + REL_TIME_CHAPTER, +}; + +struct m_rel_time { + double pos; + enum m_rel_time_type type; +}; + +struct m_color { + uint8_t r, g, b, a; +}; + +struct m_geometry { + int x, y, w, h; + bool xy_valid : 1, wh_valid : 1; + bool w_per : 1, h_per : 1; + bool x_sign : 1, y_sign : 1, x_per : 1, y_per : 1; +}; + +void m_geometry_apply(int *xpos, int *ypos, int *widw, int *widh, + int scrw, int scrh, struct m_geometry *gm); + +struct m_obj_desc { + // Name which will be used in the option string + const char *name; + // Will be printed when "help" is passed + const char *description; + // Size of the private struct + int priv_size; + // If not NULL, default values for private struct + const void *priv_defaults; + // Options which refer to members in the private struct + const struct m_option *options; + // For free use by the implementer of m_obj_list.get_desc + const void *p; + // If not NULL, options which should be set before applying other options. + // This member is usually set my m_obj_list_find() only. + // Only works if options is not NULL. + const char *init_options; + // Don't list entries with "help" + bool hidden; +}; + +// Extra definition needed for \ref m_option_type_obj_settings_list options. +struct m_obj_list { + bool (*get_desc)(struct m_obj_desc *dst, int index); + const char *description; + // Can be set to a NULL terminated array of aliases + const char *aliases[4][5]; + // Allow a trailing ",", which adds an entry with name="" + bool allow_trailer; + // Allow unknown entries, for which a dummy entry is inserted, and whose + // options are skipped and ignored. + bool allow_unknown_entries; + // If object has no options set, assume it parses options on its own. + bool legacy_hacks; +}; + +// Find entry by name +bool m_obj_list_find(struct m_obj_desc *dst, const struct m_obj_list *list, + bstr name); + +// The data type used by \ref m_option_type_obj_settings_list. +typedef struct m_obj_settings { + // Type of the object. + char *name; + // Optional user-defined name. + char *label; + // NULL terminated array of parameter/value pairs. + char **attribs; +} m_obj_settings_t; + +// A parser to set up a list of objects. +/** It creates a NULL terminated array \ref m_obj_settings. The option priv + * field (\ref m_option::priv) must point to a \ref m_obj_list_t describing + * the available object types. + */ +extern const m_option_type_t m_option_type_obj_settings_list; + +int m_obj_parse_sub_config(struct bstr opt_name, struct bstr name, + struct bstr *pstr, struct m_config *config, + int flags, char ***ret); + +struct m_opt_choice_alternatives { + char *name; + int value; +}; + +// For OPT_STRING_VALIDATE(). Behaves like m_option_type.parse(). +typedef int (*m_opt_string_validate_fn)(const m_option_t *opt, struct bstr name, + struct bstr param); + +// m_option.priv points to this if M_OPT_TYPE_USE_SUBSTRUCT is used +struct m_sub_options { + const struct m_option *opts; + size_t size; + const void *defaults; +}; + +// FIXME: backward compatibility +#define CONF_TYPE_FLAG (&m_option_type_flag) +#define CONF_TYPE_STORE (&m_option_type_store) +#define CONF_TYPE_INT (&m_option_type_int) +#define CONF_TYPE_INT64 (&m_option_type_int64) +#define CONF_TYPE_FLOAT (&m_option_type_float) +#define CONF_TYPE_DOUBLE (&m_option_type_double) +#define CONF_TYPE_STRING (&m_option_type_string) +#define CONF_TYPE_PRINT (&m_option_type_print) +#define CONF_TYPE_PRINT_FUNC (&m_option_type_print_func) +#define CONF_TYPE_SUBCONFIG (&m_option_type_subconfig) +#define CONF_TYPE_STRING_LIST (&m_option_type_string_list) +#define CONF_TYPE_IMGFMT (&m_option_type_imgfmt) +#define CONF_TYPE_FOURCC (&m_option_type_fourcc) +#define CONF_TYPE_AFMT (&m_option_type_afmt) +#define CONF_TYPE_OBJ_SETTINGS_LIST (&m_option_type_obj_settings_list) +#define CONF_TYPE_TIME (&m_option_type_time) +#define CONF_TYPE_CHOICE (&m_option_type_choice) +#define CONF_TYPE_INT_PAIR (&m_option_type_intpair) + +// Possible option values. Code is allowed to access option data without going +// through this union. It serves for self-documentation and to get minimal +// size/alignment requirements for option values in general. +union m_option_value { + int flag; // not the C type "bool"! + int store; + float float_store; + int int_; + int64_t int64; + int intpair[2]; + float float_; + double double_; + char *string; + char **string_list; + int imgfmt; + unsigned int fourcc; + int afmt; + m_obj_settings_t *obj_settings_list; + double time; + struct m_rel_time rel_time; + struct m_color color; + struct m_geometry geometry; + struct m_geometry size_box; + struct mp_chmap chmap; +}; + +//////////////////////////////////////////////////////////////////////////// + +// Option type description +struct m_option_type { + const char *name; + // Size needed for the data. + unsigned int size; + // One of M_OPT_TYPE*. + unsigned int flags; + + // Parse the data from a string. + /** It is the only required function, all others can be NULL. + * + * \param opt The option that is parsed. + * \param name The full option name. + * \param param The parameter to parse. + * may not be an argument meant for this option + * \param dst Pointer to the memory where the data should be written. + * If NULL the parameter validity should still be checked. + * \return On error a negative value is returned, on success the number + * of arguments consumed. For details see \ref OptionParserReturn. + */ + int (*parse)(const m_option_t *opt, struct bstr name, struct bstr param, + void *dst); + + // Print back a value in string form. + /** \param opt The option to print. + * \param val Pointer to the memory holding the data to be printed. + * \return An allocated string containing the text value or (void*)-1 + * on error. + */ + char *(*print)(const m_option_t *opt, const void *val); + + // Print the value in a human readable form. Unlike print(), it doesn't + // necessarily return the exact value, and is generally not parseable with + // parse(). + char *(*pretty_print)(const m_option_t *opt, const void *val); + + // Copy data between two locations. Deep copy if the data has pointers. + /** \param opt The option to copy. + * \param dst Pointer to the destination memory. + * \param src Pointer to the source memory. + */ + void (*copy)(const m_option_t *opt, void *dst, const void *src); + + // Free the data allocated for a save slot. + /** This is only needed for dynamic types like strings. + * \param dst Pointer to the data, usually a pointer that should be freed and + * set to NULL. + */ + void (*free)(void *dst); + + // Add the value add to the value in val. For types that are not numeric, + // add gives merely the direction. The wrap parameter determines whether + // the value is clipped, or wraps around to the opposite max/min. + void (*add)(const m_option_t *opt, void *val, double add, bool wrap); + + // Clamp the value in val to the option's valid value range. + // Return values: + // M_OPT_OUT_OF_RANGE: val was invalid, and modified (clamped) to be valid + // M_OPT_INVALID: val was invalid, and can't be made valid + // 0: val was already valid and is unchanged + int (*clamp)(const m_option_t *opt, void *val); +}; + +// Option description +struct m_option { + // Option name. + const char *name; + + // Reserved for higher level APIs, it shouldn't be used by parsers. + /** The suboption parser and func types do use it. They should instead + * use the priv field but this was inherited from older versions of the + * config code. + */ + void *p; + + // Option type. + const m_option_type_t *type; + + // See \ref OptionFlags. + unsigned int flags; + + // \brief Mostly useful for numeric types, the \ref M_OPT_MIN flags must + // also be set. + double min; + + // \brief Mostly useful for numeric types, the \ref M_OPT_MAX flags must + // also be set. + double max; + + // Type dependent data (for all kinds of extended settings). + /** This used to be a function pointer to hold a 'reverse to defaults' func. + * Now it can be used to pass any type of extra args needed by the parser. + */ + void *priv; + + int new; + + int offset; + + // Initialize variable to given default before parsing options + void *defval; +}; + + +// The option has a minimum set in \ref m_option::min. +#define M_OPT_MIN (1 << 0) + +// The option has a maximum set in \ref m_option::max. +#define M_OPT_MAX (1 << 1) + +// The option has a minimum and maximum in m_option::min and m_option::max. +#define M_OPT_RANGE (M_OPT_MIN | M_OPT_MAX) + +// The option is forbidden in config files. +#define M_OPT_NOCFG (1 << 2) + +// This option can't be set per-file when used with struct m_config. +#define M_OPT_GLOBAL (1 << 4) + +// The option should be set during command line pre-parsing +#define M_OPT_PRE_PARSE (1 << 6) + +// See M_OPT_TYPE_OPTIONAL_PARAM. +#define M_OPT_OPTIONAL_PARAM (1 << 10) + +// Parse C-style escapes like "\n" (for CONF_TYPE_STRING only) +#define M_OPT_PARSE_ESCAPES (1 << 11) + +// These are kept for compatibility with older code. +#define CONF_MIN M_OPT_MIN +#define CONF_MAX M_OPT_MAX +#define CONF_RANGE M_OPT_RANGE +#define CONF_NOCFG M_OPT_NOCFG +#define CONF_GLOBAL M_OPT_GLOBAL +#define CONF_PRE_PARSE M_OPT_PRE_PARSE + +// These flags are used to describe special parser capabilities or behavior. + +// Suboption parser flag. +/** When this flag is set, m_option::p should point to another m_option + * array. Only the parse function will be called. If dst is set, it should + * create/update an array of char* containg opt/val pairs. The options in + * the child array will then be set automatically by the \ref Config. + * Also note that suboptions may be directly accessed by using + * -option:subopt blah. + */ +#define M_OPT_TYPE_HAS_CHILD (1 << 0) + +// Wildcard matching flag. +/** If set the option type has a use for option names ending with a * + * (used for -aa*), this only affects the option name matching. + */ +#define M_OPT_TYPE_ALLOW_WILDCARD (1 << 1) + +// Dynamic data type. +/** This flag indicates that the data is dynamically allocated (m_option::p + * points to a pointer). It enables a little hack in the \ref Config wich + * replaces the initial value of such variables with a dynamic copy in case + * the initial value is statically allocated (pretty common with strings). + */ +#define M_OPT_TYPE_DYNAMIC (1 << 2) + +// The parameter is optional and by default no parameter is preferred. If +// ambiguous syntax is used ("--opt value"), the command line parser will +// assume that the argument takes no parameter. In config files, these +// options can be used without "=" and value. +#define M_OPT_TYPE_OPTIONAL_PARAM (1 << 3) + +// modify M_OPT_TYPE_HAS_CHILD so that m_option::p points to +// struct m_sub_options, instead of a direct m_option array. +#define M_OPT_TYPE_USE_SUBSTRUCT (1 << 4) + +///////////////////////////// Parser flags ///////////////////////////////// + +// OptionParserReturn +// +// On success parsers return a number >= 0. +// +// To indicate that MPlayer should exit without playing anything, +// parsers return M_OPT_EXIT minus the number of parameters they +// consumed: \ref M_OPT_EXIT or \ref M_OPT_EXIT-1. +// +// On error one of the following (negative) error codes is returned: + +// For use by higher level APIs when the option name is invalid. +#define M_OPT_UNKNOWN -1 + +// Returned when a parameter is needed but wasn't provided. +#define M_OPT_MISSING_PARAM -2 + +// Returned when the given parameter couldn't be parsed. +#define M_OPT_INVALID -3 + +// Returned if the value is "out of range". The exact meaning may +// vary from type to type. +#define M_OPT_OUT_OF_RANGE -4 + +// The option doesn't take a parameter. +#define M_OPT_DISALLOW_PARAM -5 + +// Returned if the parser failed for any other reason than a bad parameter. +#define M_OPT_PARSER_ERR -6 + +// Returned when MPlayer should exit. Used by various help stuff. +/** M_OPT_EXIT must be the lowest number on this list. + */ +#define M_OPT_EXIT -7 + +char *m_option_strerror(int code); + +// Find the option matching the given name in the list. +/** \ingroup Options + * This function takes the possible wildcards into account (see + * \ref M_OPT_TYPE_ALLOW_WILDCARD). + * + * \param list Pointer to an array of \ref m_option. + * \param name Name of the option. + * \return The matching option or NULL. + */ +const m_option_t *m_option_list_find(const m_option_t *list, const char *name); + +// Helper to parse options, see \ref m_option_type::parse. +static inline int m_option_parse(const m_option_t *opt, struct bstr name, + struct bstr param, void *dst) +{ + return opt->type->parse(opt, name, param, dst); +} + +// Helper to print options, see \ref m_option_type::print. +static inline char *m_option_print(const m_option_t *opt, const void *val_ptr) +{ + if (opt->type->print) + return opt->type->print(opt, val_ptr); + else + return NULL; +} + +static inline char *m_option_pretty_print(const m_option_t *opt, + const void *val_ptr) +{ + if (opt->type->pretty_print) + return opt->type->pretty_print(opt, val_ptr); + else + return m_option_print(opt, val_ptr); +} + +// Helper around \ref m_option_type::copy. +static inline void m_option_copy(const m_option_t *opt, void *dst, + const void *src) +{ + if (opt->type->copy) + opt->type->copy(opt, dst, src); +} + +// Helper around \ref m_option_type::free. +static inline void m_option_free(const m_option_t *opt, void *dst) +{ + if (opt->type->free) + opt->type->free(dst); +} + +int m_option_required_params(const m_option_t *opt); + +// Cause a compilation warning if typeof(expr) != type. +// Should be used with pointer types only. +#define MP_EXPECT_TYPE(type, expr) (0 ? (type)0 : (expr)) + +// This behaves like offsetof(type, member), but will cause a compilation +// warning if typeof(member) != expected_member_type. +// It uses some trickery to make it compile as expression. +#define MP_CHECKED_OFFSETOF(type, member, expected_member_type) \ + (offsetof(type, member) + (0 && MP_EXPECT_TYPE(expected_member_type*, \ + &((type*)0)->member))) + + +#define OPTION_LIST_SEPARATOR ',' + +#if HAVE_DOS_PATHS +#define OPTION_PATH_SEPARATOR ';' +#else +#define OPTION_PATH_SEPARATOR ':' +#endif + +#define OPTDEF_STR(s) .defval = (void *)&(char * const){s} +#define OPTDEF_INT(i) .defval = (void *)&(const int){i} + +#define OPT_GENERAL(ctype, optname, varname, flagv, ...) \ + {.name = optname, .flags = flagv, .new = 1, \ + .offset = MP_CHECKED_OFFSETOF(OPT_BASE_STRUCT, varname, ctype), \ + __VA_ARGS__} + +#define OPT_GENERAL_NOTYPE(optname, varname, flagv, ...) \ + {.name = optname, .flags = flagv, .new = 1, \ + .offset = offsetof(OPT_BASE_STRUCT, varname), \ + __VA_ARGS__} + +#define OPT_HELPER_REMOVEPAREN(...) __VA_ARGS__ + +/* The OPT_FLAG_CONSTANTS->OPT_FLAG_CONSTANTS_ kind of redirection exists to + * make the code fully standard-conforming: the C standard requires that + * __VA_ARGS__ has at least one argument (though GCC for example would accept + * 0). Thus the first OPT_FLAG_CONSTANTS is a wrapper which just adds one + * argument to ensure __VA_ARGS__ is not empty when calling the next macro. + */ + +#define OPT_FLAG(...) \ + OPT_GENERAL(int, __VA_ARGS__, .type = &m_option_type_flag, .max = 1) + +#define OPT_FLAG_CONSTANTS_(optname, varname, flags, offvalue, value, ...) \ + OPT_GENERAL(int, optname, varname, flags, \ + .min = offvalue, .max = value, __VA_ARGS__) +#define OPT_FLAG_CONSTANTS(...) \ + OPT_FLAG_CONSTANTS_(__VA_ARGS__, .type = &m_option_type_flag) + +#define OPT_FLAG_STORE(optname, varname, flags, value) \ + OPT_GENERAL(int, optname, varname, flags, .max = value, \ + .type = &m_option_type_store) + +#define OPT_FLOAT_STORE(optname, varname, flags, value) \ + OPT_GENERAL(float, optname, varname, flags, .max = value, \ + .type = &m_option_type_float_store) + +#define OPT_STRINGLIST(...) \ + OPT_GENERAL(char**, __VA_ARGS__, .type = &m_option_type_string_list) + +#define OPT_PATHLIST(...) \ + OPT_GENERAL(char**, __VA_ARGS__, .type = &m_option_type_string_list, \ + .priv = (void *)&(const char){OPTION_PATH_SEPARATOR}) + +#define OPT_INT(...) \ + OPT_GENERAL(int, __VA_ARGS__, .type = &m_option_type_int) + +#define OPT_INT64(...) \ + OPT_GENERAL(int64_t, __VA_ARGS__, .type = &m_option_type_int64) + +#define OPT_RANGE_(ctype, optname, varname, flags, minval, maxval, ...) \ + OPT_GENERAL(ctype, optname, varname, (flags) | CONF_RANGE, \ + .min = minval, .max = maxval, __VA_ARGS__) + +#define OPT_INTRANGE(...) \ + OPT_RANGE_(int, __VA_ARGS__, .type = &m_option_type_int) + +#define OPT_FLOATRANGE(...) \ + OPT_RANGE_(float, __VA_ARGS__, .type = &m_option_type_float) + +#define OPT_INTPAIR(...) \ + OPT_GENERAL_NOTYPE(__VA_ARGS__, .type = &m_option_type_intpair) + +#define OPT_FLOAT(...) \ + OPT_GENERAL(float, __VA_ARGS__, .type = &m_option_type_float) + +#define OPT_DOUBLE(...) \ + OPT_GENERAL(double, __VA_ARGS__, .type = &m_option_type_double) + +#define OPT_STRING(...) \ + OPT_GENERAL(char*, __VA_ARGS__, .type = &m_option_type_string) + +#define OPT_SETTINGSLIST(optname, varname, flags, objlist) \ + OPT_GENERAL(m_obj_settings_t*, optname, varname, flags, \ + .type = &m_option_type_obj_settings_list, \ + .priv = (void*)MP_EXPECT_TYPE(const struct m_obj_list*, objlist)) + +#define OPT_IMAGEFORMAT(...) \ + OPT_GENERAL(int, __VA_ARGS__, .type = &m_option_type_imgfmt) + +#define OPT_AUDIOFORMAT(...) \ + OPT_GENERAL(int, __VA_ARGS__, .type = &m_option_type_afmt) + +#define OPT_CHMAP(...) \ + OPT_GENERAL(struct mp_chmap, __VA_ARGS__, .type = &m_option_type_chmap) + + +#define M_CHOICES(choices) \ + .priv = (void *)&(const struct m_opt_choice_alternatives[]){ \ + OPT_HELPER_REMOVEPAREN choices, {NULL}} + +#define OPT_CHOICE(...) \ + OPT_CHOICE_(__VA_ARGS__, .type = &m_option_type_choice) +#define OPT_CHOICE_(optname, varname, flags, choices, ...) \ + OPT_GENERAL(int, optname, varname, flags, M_CHOICES(choices), __VA_ARGS__) + +// Union of choices and an int range. The choice values can be included in the +// int range, or be completely separate - both works. +#define OPT_CHOICE_OR_INT_(optname, varname, flags, minval, maxval, choices, ...) \ + OPT_GENERAL(int, optname, varname, (flags) | CONF_RANGE, \ + .min = minval, .max = maxval, \ + M_CHOICES(choices), __VA_ARGS__) +#define OPT_CHOICE_OR_INT(...) \ + OPT_CHOICE_OR_INT_(__VA_ARGS__, .type = &m_option_type_choice) + +#define OPT_TIME(...) \ + OPT_GENERAL(double, __VA_ARGS__, .type = &m_option_type_time) + +#define OPT_REL_TIME(...) \ + OPT_GENERAL(struct m_rel_time, __VA_ARGS__, .type = &m_option_type_rel_time) + +#define OPT_COLOR(...) \ + OPT_GENERAL(struct m_color, __VA_ARGS__, .type = &m_option_type_color) + +#define OPT_GEOMETRY(...) \ + OPT_GENERAL(struct m_geometry, __VA_ARGS__, .type = &m_option_type_geometry) + +#define OPT_SIZE_BOX(...) \ + OPT_GENERAL(struct m_geometry, __VA_ARGS__, .type = &m_option_type_size_box) + +#define OPT_TRACKCHOICE(name, var) \ + OPT_CHOICE_OR_INT(name, var, 0, 0, 8190, ({"no", -2}, {"auto", -1})) + +#define OPT_STRING_VALIDATE_(optname, varname, flags, validate_fn, ...) \ + OPT_GENERAL(char*, optname, varname, flags, __VA_ARGS__, \ + .priv = MP_EXPECT_TYPE(m_opt_string_validate_fn, validate_fn)) +#define OPT_STRING_VALIDATE(...) \ + OPT_STRING_VALIDATE_(__VA_ARGS__, .type = &m_option_type_string) + +// subconf must have the type struct m_sub_options. +// All sub-options are prefixed with "name-" and are added to the current +// (containing) option list. +// If name is "", add the sub-options directly instead. +// varname refers to the field, that must be a pointer to a field described by +// the subconf struct. +#define OPT_SUBSTRUCT(name, varname, subconf, flagv) \ + OPT_GENERAL_NOTYPE(name, varname, flagv, \ + .type = &m_option_type_subconfig_struct, \ + .priv = (void*)&subconf) + +#endif /* MPLAYER_M_OPTION_H */ diff --git a/mpvcore/m_property.c b/mpvcore/m_property.c new file mode 100644 index 0000000000..f334b6fe1f --- /dev/null +++ b/mpvcore/m_property.c @@ -0,0 +1,369 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +/// \file +/// \ingroup Properties + +#include "config.h" + +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <inttypes.h> +#include <assert.h> + +#include <libavutil/common.h> + +#include "talloc.h" +#include "core/m_option.h" +#include "m_property.h" +#include "core/mp_msg.h" +#include "core/mp_common.h" + +const struct m_option_type m_option_type_dummy = { + .name = "Unknown", + .flags = M_OPT_TYPE_ALLOW_WILDCARD, // make "vf*" property work +}; + +struct legacy_prop { + const char *old, *new; +}; +static const struct legacy_prop legacy_props[] = { + {"switch_video", "video"}, + {"switch_audio", "audio"}, + {"switch_program", "program"}, + {"framedropping", "framedrop"}, + {"osdlevel", "osd-level"}, + {0} +}; + +static bool translate_legacy_property(const char *name, char *buffer, + size_t buffer_size) +{ + if (strlen(name) + 1 > buffer_size) + return false; + + const char *old_name = name; + + for (int n = 0; legacy_props[n].new; n++) { + if (strcmp(name, legacy_props[n].old) == 0) { + name = legacy_props[n].new; + break; + } + } + + snprintf(buffer, buffer_size, "%s", name); + + // Old names used "_" instead of "-" + for (int n = 0; buffer[n]; n++) { + if (buffer[n] == '_') + buffer[n] = '-'; + } + + if (strcmp(old_name, buffer) != 0) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "Warning: property '%s' is deprecated, " + "replaced with '%s'. Fix your input.conf!\n", old_name, buffer); + } + + return true; +} + +static int do_action(const m_option_t *prop_list, const char *name, + int action, void *arg, void *ctx) +{ + const char *sep; + const m_option_t *prop; + struct m_property_action_arg ka; + if ((sep = strchr(name, '/')) && sep[1]) { + int len = sep - name; + char base[len + 1]; + memcpy(base, name, len); + base[len] = 0; + prop = m_option_list_find(prop_list, base); + ka = (struct m_property_action_arg) { + .key = sep + 1, + .action = action, + .arg = arg, + }; + action = M_PROPERTY_KEY_ACTION; + arg = &ka; + } else + prop = m_option_list_find(prop_list, name); + if (!prop) + return M_PROPERTY_UNKNOWN; + int (*control)(const m_option_t*, int, void*, void*) = prop->p; + int r = control(prop, action, arg, ctx); + if (action == M_PROPERTY_GET_TYPE && r < 0 && + prop->type != &m_option_type_dummy) + { + *(struct m_option *)arg = *prop; + return M_PROPERTY_OK; + } + return r; +} + +int m_property_do(const m_option_t *prop_list, const char *in_name, + int action, void *arg, void *ctx) +{ + union m_option_value val = {0}; + int r; + + char name[64]; + if (!translate_legacy_property(in_name, name, sizeof(name))) + return M_PROPERTY_UNKNOWN; + + struct m_option opt = {0}; + r = do_action(prop_list, name, M_PROPERTY_GET_TYPE, &opt, ctx); + if (r <= 0) + return r; + assert(opt.type); + + switch (action) { + case M_PROPERTY_PRINT: { + if ((r = do_action(prop_list, name, M_PROPERTY_PRINT, arg, ctx)) >= 0) + return r; + // Fallback to m_option + if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0) + return r; + char *str = m_option_pretty_print(&opt, &val); + m_option_free(&opt, &val); + *(char **)arg = str; + return str != NULL; + } + case M_PROPERTY_GET_STRING: { + if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0) + return r; + char *str = m_option_print(&opt, &val); + m_option_free(&opt, &val); + *(char **)arg = str; + return str != NULL; + } + case M_PROPERTY_SET_STRING: { + // (reject 0 return value: success, but empty string with flag) + if (m_option_parse(&opt, bstr0(name), bstr0(arg), &val) <= 0) + return M_PROPERTY_ERROR; + r = do_action(prop_list, name, M_PROPERTY_SET, &val, ctx); + m_option_free(&opt, &val); + return r; + } + case M_PROPERTY_SWITCH: { + struct m_property_switch_arg *sarg = arg; + if ((r = do_action(prop_list, name, M_PROPERTY_SWITCH, arg, ctx)) != + M_PROPERTY_NOT_IMPLEMENTED) + return r; + // Fallback to m_option + if (!opt.type->add) + return M_PROPERTY_NOT_IMPLEMENTED; + if ((r = do_action(prop_list, name, M_PROPERTY_GET, &val, ctx)) <= 0) + return r; + opt.type->add(&opt, &val, sarg->inc, sarg->wrap); + r = do_action(prop_list, name, M_PROPERTY_SET, &val, ctx); + m_option_free(&opt, &val); + return r; + } + case M_PROPERTY_SET: { + if (!opt.type->clamp) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "Property '%s' without clamp().\n", + name); + } else { + m_option_copy(&opt, &val, arg); + r = opt.type->clamp(&opt, arg); + m_option_free(&opt, &val); + if (r != 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, + "Property '%s': invalid value.\n", name); + return M_PROPERTY_ERROR; + } + } + return do_action(prop_list, name, M_PROPERTY_SET, arg, ctx); + } + default: + return do_action(prop_list, name, action, arg, ctx); + } +} + +static int m_property_do_bstr(const m_option_t *prop_list, bstr name, + int action, void *arg, void *ctx) +{ + char name0[64]; + if (name.len >= sizeof(name0)) + return M_PROPERTY_UNKNOWN; + snprintf(name0, sizeof(name0), "%.*s", BSTR_P(name)); + return m_property_do(prop_list, name0, action, arg, ctx); +} + +static void append_str(char **s, int *len, bstr append) +{ + MP_TARRAY_GROW(NULL, *s, *len + append.len); + memcpy(*s + *len, append.start, append.len); + *len = *len + append.len; +} + +char *m_properties_expand_string(const m_option_t *prop_list, char *str0, + void *ctx) +{ + char *ret = NULL; + int ret_len = 0; + bool skip = false; + int level = 0, skip_level = 0; + bstr str = bstr0(str0); + + while (str.len) { + if (level > 0 && bstr_eatstart0(&str, "}")) { + if (skip && level <= skip_level) + skip = false; + level--; + } else if (bstr_startswith0(str, "${") && bstr_find0(str, "}") >= 0) { + str = bstr_cut(str, 2); + level++; + + // Assume ":" and "}" can't be part of the property name + // => if ":" comes before "}", it must be for the fallback + int term_pos = bstrcspn(str, ":}"); + bstr name = bstr_splice(str, 0, term_pos < 0 ? str.len : term_pos); + str = bstr_cut(str, term_pos); + bool have_fallback = bstr_eatstart0(&str, ":"); + + if (!skip) { + bool cond_yes = bstr_eatstart0(&name, "?"); + bool cond_no = !cond_yes && bstr_eatstart0(&name, "!"); + bool raw = bstr_eatstart0(&name, "="); + int method = (raw || cond_yes || cond_no) + ? M_PROPERTY_GET_STRING : M_PROPERTY_PRINT; + + char *s = NULL; + int r = m_property_do_bstr(prop_list, name, method, &s, ctx); + if (cond_yes || cond_no) { + skip = (!!s != cond_yes); + } else { + skip = !!s; + char *append = s; + if (!s && !have_fallback && !raw) { + append = r == M_PROPERTY_UNAVAILABLE + ? "(unavailable)" : "(error)"; + } + append_str(&ret, &ret_len, bstr0(append)); + } + talloc_free(s); + if (skip) + skip_level = level; + } + } else if (level == 0 && bstr_eatstart0(&str, "$>")) { + append_str(&ret, &ret_len, str); + break; + } else { + char c; + + // Other combinations, e.g. "$x", are added verbatim + if (bstr_eatstart0(&str, "$$")) { + c = '$'; + } else if (bstr_eatstart0(&str, "$}")) { + c = '}'; + } else { + c = str.start[0]; + str = bstr_cut(str, 1); + } + + if (!skip) + MP_TARRAY_APPEND(NULL, ret, ret_len, c); + } + } + + MP_TARRAY_APPEND(NULL, ret, ret_len, '\0'); + return ret; +} + +void m_properties_print_help_list(const m_option_t *list) +{ + char min[50], max[50]; + int i, count = 0; + + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, + "\n Name Type Min Max\n\n"); + for (i = 0; list[i].name; i++) { + const m_option_t *opt = &list[i]; + if (opt->flags & M_OPT_MIN) + sprintf(min, "%-8.0f", opt->min); + else + strcpy(min, "No"); + if (opt->flags & M_OPT_MAX) + sprintf(max, "%-8.0f", opt->max); + else + strcpy(max, "No"); + mp_msg(MSGT_CFGPARSER, MSGL_INFO, + " %-20.20s %-15.15s %-10.10s %-10.10s\n", + opt->name, + opt->type->name, + min, + max); + count++; + } + mp_tmsg(MSGT_CFGPARSER, MSGL_INFO, "\nTotal: %d properties\n", count); +} + +int m_property_int_ro(const m_option_t *prop, int action, + void *arg, int var) +{ + if (action == M_PROPERTY_GET) { + *(int *)arg = var; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +int m_property_int64_ro(const struct m_option* prop, int action, void* arg, + int64_t var) +{ + if (action == M_PROPERTY_GET) { + *(int64_t *)arg = var; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +int m_property_float_ro(const m_option_t *prop, int action, + void *arg, float var) +{ + if (action == M_PROPERTY_GET) { + *(float *)arg = var; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +int m_property_double_ro(const m_option_t *prop, int action, + void *arg, double var) +{ + if (action == M_PROPERTY_GET) { + *(double *)arg = var; + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + +int m_property_strdup_ro(const struct m_option* prop, int action, void* arg, + const char *var) +{ + if (action == M_PROPERTY_GET) { + if (!var) + return M_PROPERTY_UNAVAILABLE; + *(char **)arg = talloc_strdup(NULL, var); + return M_PROPERTY_OK; + } + return M_PROPERTY_NOT_IMPLEMENTED; +} diff --git a/mpvcore/m_property.h b/mpvcore/m_property.h new file mode 100644 index 0000000000..b471b94ecd --- /dev/null +++ b/mpvcore/m_property.h @@ -0,0 +1,142 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_M_PROPERTY_H +#define MPLAYER_M_PROPERTY_H + +#include <stdbool.h> +#include <stdint.h> + +struct m_option; + +extern const struct m_option_type m_option_type_dummy; + +enum mp_property_action { + // Get the property type. This defines the fundamental data type read from + // or written to the property. + // If unimplemented, the m_option entry that defines the property is used. + // arg: m_option* + M_PROPERTY_GET_TYPE, + + // Get the current value. + // arg: pointer to a variable of the type according to the property type + M_PROPERTY_GET, + + // Set a new value. The property wrapper will make sure that only valid + // values are set (e.g. according to the property type's min/max range). + // If unimplemented, the property is read-only. + // arg: pointer to a variable of the type according to the property type + M_PROPERTY_SET, + + // Get human readable string representing the current value. + // If unimplemented, the property wrapper uses the property type as + // fallback. + // arg: char** + M_PROPERTY_PRINT, + + // Switch the property up/down by a given value. + // If unimplemented, the property wrapper uses the property type as + // fallback. + // arg: struct m_property_switch_arg* + M_PROPERTY_SWITCH, + + // Get a string containing a parsable representation. + // Can't be overridden by property implementations. + // arg: char** + M_PROPERTY_GET_STRING, + + // Set a new value from a string. The property wrapper parses this using the + // parse function provided by the property type. + // Can't be overridden by property implementations. + // arg: char* + M_PROPERTY_SET_STRING, + + // Pass down an action to a sub-property. + // arg: struct m_property_action_arg* + M_PROPERTY_KEY_ACTION, +}; + +// Argument for M_PROPERTY_SWITCH +struct m_property_switch_arg { + double inc; // value to add to property, or cycle direction + bool wrap; // whether value should wrap around on over/underflow +}; + +// Argument for M_PROPERTY_KEY_ACTION +struct m_property_action_arg { + const char* key; + int action; + void* arg; +}; + +enum mp_property_return { + // Returned on success. + M_PROPERTY_OK = 1, + + // Returned on error. + M_PROPERTY_ERROR = 0, + + // Returned when the property can't be used, for example video related + // properties while playing audio only. + M_PROPERTY_UNAVAILABLE = -1, + + // Returned if the requested action is not implemented. + M_PROPERTY_NOT_IMPLEMENTED = -2, + + // Returned when asking for a property that doesn't exist. + M_PROPERTY_UNKNOWN = -3, +}; + +// Access a property. +// action: one of m_property_action +// ctx: opaque value passed through to property implementation +// returns: one of mp_property_return +int m_property_do(const struct m_option* prop_list, const char* property_name, + int action, void* arg, void *ctx); + +// Print a list of properties. +void m_properties_print_help_list(const struct m_option* list); + +// Expand a property string. +// This function allows to print strings containing property values. +// ${NAME} is expanded to the value of property NAME. +// If NAME starts with '=', use the raw value of the property. +// ${NAME:STR} expands to the property, or STR if the property is not +// available. +// ${?NAME:STR} expands to STR if the property is available. +// ${!NAME:STR} expands to STR if the property is not available. +// General syntax: "${" ["?" | "!"] ["="] NAME ":" STR "}" +// STR is recursively expanded using the same rules. +// "$$" can be used to escape "$", and "$}" to escape "}". +// "$>" disables parsing of "$" for the rest of the string. +char* m_properties_expand_string(const struct m_option* prop_list, char *str, + void *ctx); + +// Trivial helpers for implementing properties. +int m_property_int_ro(const struct m_option* prop, int action, void* arg, + int var); +int m_property_int64_ro(const struct m_option* prop, int action, void* arg, + int64_t var); +int m_property_float_ro(const struct m_option* prop, int action, void* arg, + float var); +int m_property_double_ro(const struct m_option* prop, int action, void* arg, + double var); +int m_property_strdup_ro(const struct m_option* prop, int action, void* arg, + const char *var); + +#endif /* MPLAYER_M_PROPERTY_H */ diff --git a/mpvcore/mp_common.c b/mpvcore/mp_common.c new file mode 100644 index 0000000000..03e3e988fd --- /dev/null +++ b/mpvcore/mp_common.c @@ -0,0 +1,125 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <libavutil/common.h> + +#include "talloc.h" +#include "core/bstr.h" +#include "core/mp_common.h" + +char *mp_format_time(double time, bool fractions) +{ + if (time == MP_NOPTS_VALUE) + return talloc_strdup(NULL, "unknown"); + char sign[2] = {0}; + if (time < 0) { + time = -time; + sign[0] = '-'; + } + long long int itime = time; + long long int h, m, s; + s = itime; + h = s / 3600; + s -= h * 3600; + m = s / 60; + s -= m * 60; + char *res = talloc_asprintf(NULL, "%s%02lld:%02lld:%02lld", sign, h, m, s); + if (fractions) + res = talloc_asprintf_append(res, ".%03d", + (int)((time - itime) * 1000)); + return res; +} + +// Set rc to the union of rc and rc2 +void mp_rect_union(struct mp_rect *rc, const struct mp_rect *rc2) +{ + rc->x0 = FFMIN(rc->x0, rc2->x0); + rc->y0 = FFMIN(rc->y0, rc2->y0); + rc->x1 = FFMAX(rc->x1, rc2->x1); + rc->y1 = FFMAX(rc->y1, rc2->y1); +} + +// Set rc to the intersection of rc and src. +// Return false if the result is empty. +bool mp_rect_intersection(struct mp_rect *rc, const struct mp_rect *rc2) +{ + rc->x0 = FFMAX(rc->x0, rc2->x0); + rc->y0 = FFMAX(rc->y0, rc2->y0); + rc->x1 = FFMIN(rc->x1, rc2->x1); + rc->y1 = FFMIN(rc->y1, rc2->y1); + + return rc->x1 > rc->x0 && rc->y1 > rc->y0; +} + +// Encode the unicode codepoint as UTF-8, and append to the end of the +// talloc'ed buffer. +char *mp_append_utf8_buffer(char *buffer, uint32_t codepoint) +{ + char data[8]; + uint8_t tmp; + char *output = data; + PUT_UTF8(codepoint, tmp, *output++ = tmp;); + return talloc_strndup_append_buffer(buffer, data, output - data); +} + +// Parse a C-style escape beginning at code, and append the result to *str +// using talloc. The input string (*code) must point to the first character +// after the initial '\', and after parsing *code is set to the first character +// after the current escape. +// On error, false is returned, and all input remains unchanged. +bool mp_parse_escape(bstr *code, char **str) +{ + if (code->len < 1) + return false; + char replace = 0; + switch (code->start[0]) { + case '"': replace = '"'; break; + case '\\': replace = '\\'; break; + case 'b': replace = '\b'; break; + case 'f': replace = '\f'; break; + case 'n': replace = '\n'; break; + case 'r': replace = '\r'; break; + case 't': replace = '\t'; break; + case 'e': replace = '\x1b'; break; + case '\'': replace = '\''; break; + } + if (replace) { + *str = talloc_strndup_append_buffer(*str, &replace, 1); + *code = bstr_cut(*code, 1); + return true; + } + if (code->start[0] == 'x' && code->len >= 3) { + bstr num = bstr_splice(*code, 1, 3); + char c = bstrtoll(num, &num, 16); + if (!num.len) + return false; + *str = talloc_strndup_append_buffer(*str, &c, 1); + *code = bstr_cut(*code, 3); + return true; + } + if (code->start[0] == 'u' && code->len >= 5) { + bstr num = bstr_splice(*code, 1, 5); + int c = bstrtoll(num, &num, 16); + if (num.len) + return false; + *str = mp_append_utf8_buffer(*str, c); + *code = bstr_cut(*code, 5); + return true; + } + return false; +} diff --git a/mpvcore/mp_common.h b/mpvcore/mp_common.h new file mode 100644 index 0000000000..71e24604ec --- /dev/null +++ b/mpvcore/mp_common.h @@ -0,0 +1,66 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_MPCOMMON_H +#define MPLAYER_MPCOMMON_H + +#include <stdlib.h> +#include <stdbool.h> +#include <stdint.h> + +#include "compat/compiler.h" +#include "core/mp_talloc.h" + +// both int64_t and double should be able to represent this exactly +#define MP_NOPTS_VALUE (-1LL<<63) + +#define MP_CONCAT_(a, b) a ## b +#define MP_CONCAT(a, b) MP_CONCAT_(a, b) + +#define ROUND(x) ((int)((x) < 0 ? (x) - 0.5 : (x) + 0.5)) + +#define MPMAX(a, b) ((a) > (b) ? (a) : (b)) +#define MPMIN(a, b) ((a) > (b) ? (b) : (a)) +#define MP_ARRAY_SIZE(s) (sizeof(s) / sizeof((s)[0])) + +#define CONTROL_OK 1 +#define CONTROL_TRUE 1 +#define CONTROL_FALSE 0 +#define CONTROL_UNKNOWN -1 +#define CONTROL_ERROR -2 +#define CONTROL_NA -3 + +extern const char *mplayer_version; +extern const char *mplayer_builddate; + +char *mp_format_time(double time, bool fractions); + +struct mp_rect { + int x0, y0; + int x1, y1; +}; + +void mp_rect_union(struct mp_rect *rc, const struct mp_rect *src); +bool mp_rect_intersection(struct mp_rect *rc, const struct mp_rect *rc2); + +char *mp_append_utf8_buffer(char *buffer, uint32_t codepoint); + +struct bstr; +bool mp_parse_escape(struct bstr *code, char **str); + +#endif /* MPLAYER_MPCOMMON_H */ diff --git a/mpvcore/mp_core.h b/mpvcore/mp_core.h new file mode 100644 index 0000000000..126c646a0b --- /dev/null +++ b/mpvcore/mp_core.h @@ -0,0 +1,343 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_MP_CORE_H +#define MPLAYER_MP_CORE_H + +#include <stdbool.h> + +#include "core/options.h" +#include "audio/mixer.h" +#include "demux/demux.h" + +// definitions used internally by the core player code + +#define INITIALIZED_VO 1 +#define INITIALIZED_AO 2 +#define INITIALIZED_VOL 4 +#define INITIALIZED_GETCH2 8 +#define INITIALIZED_STREAM 64 +#define INITIALIZED_DEMUXER 512 +#define INITIALIZED_ACODEC 1024 +#define INITIALIZED_VCODEC 2048 +#define INITIALIZED_SUB 4096 +#define INITIALIZED_ALL 0xFFFF + + +enum stop_play_reason { + KEEP_PLAYING = 0, // must be 0, numeric values of others do not matter + AT_END_OF_FILE, // file has ended normally, prepare to play next + PT_NEXT_ENTRY, // prepare to play next entry in playlist + PT_CURRENT_ENTRY, // prepare to play mpctx->playlist->current + PT_STOP, // stop playback, clear playlist + PT_RESTART, // restart previous file + PT_QUIT, // stop playback, quit player +}; + +enum exit_reason { + EXIT_NONE, + EXIT_QUIT, + EXIT_PLAYED, + EXIT_ERROR, + EXIT_NOTPLAYED, + EXIT_SOMENOTPLAYED +}; + +struct timeline_part { + double start; + double source_start; + struct demuxer *source; +}; + +struct chapter { + double start; + char *name; +}; + +enum mp_osd_seek_info { + OSD_SEEK_INFO_BAR = 1, + OSD_SEEK_INFO_TEXT = 2, + OSD_SEEK_INFO_CHAPTER_TEXT = 4, + OSD_SEEK_INFO_EDITION = 8, +}; + +struct track { + enum stream_type type; + // The type specific ID, also called aid (audio), sid (subs), vid (video). + // For UI purposes only; this ID doesn't have anything to do with any + // IDs coming from demuxers or container files. + int user_tid; + + // Same as stream->demuxer_id. -1 if not set. + int demuxer_id; + + char *title; + bool default_track; + bool attached_picture; + char *lang; + + // If this track is from an external file (e.g. subtitle file). + bool is_external; + char *external_filename; + bool auto_loaded; + + // If the track's stream changes with the timeline (ordered chapters). + bool under_timeline; + + // NULL if not backed by a demuxer (e.g. external subtitles). + // Value can change if under_timeline==true. + struct demuxer *demuxer; + // Invariant: (!demuxer && !stream) || stream->demuxer == demuxer + struct sh_stream *stream; + + // For external subtitles, which are read fully on init. Do not attempt + // to read packets from them. + bool preloaded; +}; + +enum { + MAX_NUM_VO_PTS = 100, +}; + +typedef struct MPContext { + struct mpv_global *global; + struct MPOpts *opts; + struct mp_log *log; + struct m_config *mconfig; + struct input_ctx *input; + struct osd_state *osd; + struct mp_osd_msg *osd_msg_stack; + char *terminal_osd_text; + + int add_osd_seek_info; // bitfield of enum mp_osd_seek_info + double osd_visible; // for the osd bar only + int osd_function; + double osd_function_visible; + double osd_last_update; + + struct playlist *playlist; + char *filename; // currently playing file + struct mp_resolve_result *resolve_result; + enum stop_play_reason stop_play; + unsigned int initialized_flags; // which subsystems have been initialized + + // Return code to use with PT_QUIT + enum exit_reason quit_player_rc; + int quit_custom_rc; + bool has_quit_custom_rc; + bool error_playing; + + struct demuxer **sources; + int num_sources; + + struct timeline_part *timeline; + int num_timeline_parts; + int timeline_part; + // NOTE: even if num_chapters==0, chapters being not NULL signifies presence + // of chapter metadata + struct chapter *chapters; + int num_chapters; + double video_offset; + + struct stream *stream; + struct demuxer *demuxer; + + struct track **tracks; + int num_tracks; + + // Selected tracks. NULL if no track selected. + struct track *current_track[STREAM_TYPE_COUNT]; + + struct sh_stream *sh[STREAM_TYPE_COUNT]; + struct sh_audio *sh_audio; // same as sh[STREAM_AUDIO]->audio + struct sh_video *sh_video; // same as sh[STREAM_VIDEO]->video + struct sh_sub *sh_sub; // same as sh[STREAM_SUB]->sub + + // Uses: accessing metadata (consider ordered chapters case, where the main + // demuxer defines metadata), or special purpose demuxers like TV. + struct demuxer *master_demuxer; + + mixer_t mixer; + struct ao *ao; + struct vo *video_out; + + /* We're starting playback from scratch or after a seek. Show first + * video frame immediately and reinitialize sync. */ + bool restart_playback; + /* Set if audio should be timed to start with video frame after seeking, + * not set when e.g. playing cover art */ + bool sync_audio_to_video; + /* After playback restart (above) or audio stream change, adjust audio + * stream by cutting samples or adding silence at the beginning to make + * audio playback position match video position. */ + bool syncing_audio; + bool hrseek_active; + bool hrseek_framedrop; + double hrseek_pts; + // AV sync: the next frame should be shown when the audio out has this + // much (in seconds) buffered data left. Increased when more data is + // written to the ao, decreased when moving to the next frame. + // In the audio-only case used as a timer since the last seek + // by the audio CPU usage meter. + double delay; + // AV sync: time until next frame should be shown + double time_frame; + // How long the last vo flip() call took. Used to adjust timing with + // the goal of making flip() calls finish (rather than start) at the + // specified time. + double last_vo_flip_duration; + // How much video timing has been changed to make it match the audio + // timeline. Used for status line information only. + double total_avsync_change; + // Total number of dropped frames that were "approved" to be dropped. + // Actual dropping depends on --framedrop and decoder internals. + int drop_frame_cnt; + // Number of frames dropped in a row. + int dropped_frames; + // A-V sync difference when last frame was displayed. Kept to display + // the same value if the status line is updated at a time where no new + // video frame is shown. + double last_av_difference; + /* timestamp of video frame currently visible on screen + * (or at least queued to be flipped by VO) */ + double video_pts; + double last_seek_pts; + // As video_pts, but is not reset when seeking away. (For the very short + // period of time until a new frame is decoded and shown.) + double last_vo_pts; + // Video PTS, or audio PTS if video has ended. + double playback_pts; + + // History of video frames timestamps that were queued in the VO + // This includes even skipped frames during hr-seek + double vo_pts_history_pts[MAX_NUM_VO_PTS]; + // Whether the PTS at vo_pts_history[n] is after a seek reset + uint64_t vo_pts_history_seek[MAX_NUM_VO_PTS]; + uint64_t vo_pts_history_seek_ts; + uint64_t backstep_start_seek_ts; + bool backstep_active; + + double audio_delay; + + double last_heartbeat; + double last_metadata_update; + + double mouse_timer; + unsigned int mouse_event_ts; + int mouse_waiting_hide; + + // used to prevent hanging in some error cases + double start_timestamp; + + // Timestamp from the last time some timing functions read the + // current time, in (occasionally wrapping) microseconds. Used + // to turn a new time value to a delta from last time. + int64_t last_time; + + // Used to communicate the parameters of a seek between parts + struct seek_params { + enum seek_type { + MPSEEK_NONE, MPSEEK_RELATIVE, MPSEEK_ABSOLUTE, MPSEEK_FACTOR + } type; + double amount; + int exact; // -1 = disable, 0 = default, 1 = enable + // currently not set by commands, only used internally by seek() + int direction; // -1 = backward, 0 = default, 1 = forward + } seek; + + /* Heuristic for relative chapter seeks: keep track which chapter + * the user wanted to go to, even if we aren't exactly within the + * boundaries of that chapter due to an inaccurate seek. */ + int last_chapter_seek; + double last_chapter_pts; + + struct ass_library *ass_library; + + int last_dvb_step; + int dvbin_reopen; + + bool paused; + // step this many frames, then pause + int step_frames; + // Counted down each frame, stop playback if 0 is reached. (-1 = disable) + int max_frames; + bool playing_msg_shown; + + bool paused_for_cache; + + // Set after showing warning about decoding being too slow for realtime + // playback rate. Used to avoid showing it multiple times. + bool drop_message_shown; + + struct screenshot_ctx *screenshot_ctx; + + char *track_layout_hash; + + struct encode_lavc_context *encode_lavc_ctx; +} MPContext; + + +// should not be global +extern FILE *edl_fd; +// These appear in options list +extern int forced_subs_only; + +void uninit_player(struct MPContext *mpctx, unsigned int mask); +void reinit_audio_chain(struct MPContext *mpctx); +double playing_audio_pts(struct MPContext *mpctx); +struct track *mp_add_subtitles(struct MPContext *mpctx, char *filename, int noerr); +int reinit_video_chain(struct MPContext *mpctx); +int reinit_video_filters(struct MPContext *mpctx); +int reinit_audio_filters(struct MPContext *mpctx); +void pause_player(struct MPContext *mpctx); +void unpause_player(struct MPContext *mpctx); +void add_step_frame(struct MPContext *mpctx, int dir); +void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount, + int exact); +bool mp_seek_chapter(struct MPContext *mpctx, int chapter); +double get_time_length(struct MPContext *mpctx); +double get_start_time(struct MPContext *mpctx); +double get_current_time(struct MPContext *mpctx); +int get_percent_pos(struct MPContext *mpctx); +double get_current_pos_ratio(struct MPContext *mpctx, bool use_range); +int get_current_chapter(struct MPContext *mpctx); +char *chapter_display_name(struct MPContext *mpctx, int chapter); +char *chapter_name(struct MPContext *mpctx, int chapter); +double chapter_start_time(struct MPContext *mpctx, int chapter); +int get_chapter_count(struct MPContext *mpctx); +void mp_switch_track(struct MPContext *mpctx, enum stream_type type, + struct track *track); +struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type, + int tid); +bool mp_remove_track(struct MPContext *mpctx, struct track *track); +struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction); +int mp_get_cache_percent(struct MPContext *mpctx); +void mp_write_watch_later_conf(struct MPContext *mpctx); +void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e); +void mp_force_video_refresh(struct MPContext *mpctx); + +void mp_print_version(int always); + +// timeline/tl_matroska.c +void build_ordered_chapter_timeline(struct MPContext *mpctx); +// timeline/tl_edl.c +void build_edl_timeline(struct MPContext *mpctx); +// timeline/tl_cue.c +void build_cue_timeline(struct MPContext *mpctx); + +#endif /* MPLAYER_MP_CORE_H */ diff --git a/mpvcore/mp_memory_barrier.h b/mpvcore/mp_memory_barrier.h new file mode 100644 index 0000000000..e27825de8f --- /dev/null +++ b/mpvcore/mp_memory_barrier.h @@ -0,0 +1,23 @@ +/* + * This file is part of mpv. + * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com> + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +// At this point both gcc and clang had __sync_synchronize support for some +// time. We only support a full memory barrier. + +#define mp_memory_barrier() __sync_synchronize() +#define mp_atomic_add_and_fetch(a, b) __sync_add_and_fetch(a, b) diff --git a/mpvcore/mp_msg.c b/mpvcore/mp_msg.c new file mode 100644 index 0000000000..ff611451c2 --- /dev/null +++ b/mpvcore/mp_msg.c @@ -0,0 +1,471 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <stdarg.h> +#include <string.h> +#include <unistd.h> +#include <assert.h> + +#include "talloc.h" + +#include "config.h" +#include "core/mpv_global.h" +#include "osdep/getch2.h" +#include "osdep/io.h" + +#ifdef CONFIG_GETTEXT +#include <locale.h> +#include <libintl.h> +#endif + +#ifndef __MINGW32__ +#include <signal.h> +#endif + +#include "core/mp_msg.h" + +bool mp_msg_stdout_in_use = 0; + +struct mp_log_root { + /* This should, at some point, contain all mp_msg related state, instead + * of having global variables (at least as long as we don't want to + * control the terminal, which is global anyway). But for now, there is + * not much. */ + struct mpv_global *global; +}; + +struct mp_log { + struct mp_log_root *root; + const char *prefix; + const char *verbose_prefix; + int legacy_mod; +}; + +// should not exist +static bool initialized; +static struct mp_log *legacy_logs[MSGT_MAX]; + +/* maximum message length of mp_msg */ +#define MSGSIZE_MAX 6144 + +#ifdef _WIN32 +#define WIN32_LEAN_AND_MEAN +#include <windows.h> +#include <io.h> +#define hSTDOUT GetStdHandle(STD_OUTPUT_HANDLE) +#define hSTDERR GetStdHandle(STD_ERROR_HANDLE) +static short stdoutAttrs = 0; +static const unsigned char ansi2win32[10] = { + 0, + FOREGROUND_RED, + FOREGROUND_GREEN, + FOREGROUND_GREEN | FOREGROUND_RED, + FOREGROUND_BLUE, + FOREGROUND_BLUE | FOREGROUND_RED, + FOREGROUND_BLUE | FOREGROUND_GREEN, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED, + FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_RED +}; +#endif + +int mp_msg_levels[MSGT_MAX]; // verbose level of this module. initialized to -2 +int mp_msg_level_all = MSGL_STATUS; +int verbose = 0; +int mp_msg_color = 1; +int mp_msg_module = 0; +int mp_msg_cancolor = 0; + +static int mp_msg_docolor(void) { + return mp_msg_cancolor && mp_msg_color; +} + +static void mp_msg_do_init(void){ +#ifdef _WIN32 + CONSOLE_SCREEN_BUFFER_INFO cinfo; + DWORD cmode = 0; + GetConsoleMode(hSTDOUT, &cmode); + cmode |= (ENABLE_PROCESSED_OUTPUT | ENABLE_WRAP_AT_EOL_OUTPUT); + SetConsoleMode(hSTDOUT, cmode); + SetConsoleMode(hSTDERR, cmode); + GetConsoleScreenBufferInfo(hSTDOUT, &cinfo); + stdoutAttrs = cinfo.wAttributes; +#endif +#ifndef __MINGW32__ + struct sigaction sa; + sa.sa_handler = SIG_IGN; + sa.sa_flags = 0; + sigemptyset(&sa.sa_mask); + sigaction(SIGTTOU, &sa, NULL); // just write to stdout if you have to +#endif + int i; + char *env = getenv("MPV_VERBOSE"); + if (env) + verbose = atoi(env); + for(i=0;i<MSGT_MAX;i++) mp_msg_levels[i] = -2; + mp_msg_cancolor = isatty(fileno(stdout)); + mp_msg_levels[MSGT_IDENTIFY] = -1; // no -identify output by default +#ifdef CONFIG_GETTEXT + textdomain("mpv"); + char *localedir = getenv("MPV_LOCALEDIR"); + if (localedir == NULL && strlen(MPLAYER_LOCALEDIR)) + localedir = MPLAYER_LOCALEDIR; + bindtextdomain("mpv", localedir); + bind_textdomain_codeset("mpv", "UTF-8"); +#endif +} + +int mp_msg_test(int mod, int lev) +{ +#ifndef __MINGW32__ + if (lev == MSGL_STATUS) { + // skip status line output if stderr is a tty but in background + if (isatty(2) && tcgetpgrp(2) != getpgrp()) + return false; + } +#endif + return lev <= (mp_msg_levels[mod] == -2 ? mp_msg_level_all + verbose : mp_msg_levels[mod]); +} + +bool mp_msg_test_log(struct mp_log *log, int lev) +{ + return mp_msg_test(log->legacy_mod, lev); +} + +static void set_msg_color(FILE* stream, int lev) +{ + static const int v_colors[10] = {9, 1, 3, 3, -1, -1, 2, 8, 8, 8}; + int c = v_colors[lev]; +#ifdef MP_ANNOY_ME + /* that's only a silly color test */ + { + int c; + static int flag = 1; + if (flag) + for(c = 0; c < 24; c++) + printf("\033[%d;3%dm*** COLOR TEST %d ***\n", c>7, c&7, c); + flag = 0; + } +#endif + if (mp_msg_docolor()) + { +#if defined(_WIN32) && !defined(__CYGWIN__) + HANDLE *wstream = stream == stderr ? hSTDERR : hSTDOUT; + if (c == -1) + c = 7; + SetConsoleTextAttribute(wstream, ansi2win32[c] | FOREGROUND_INTENSITY); +#else + if (c == -1) { + fprintf(stream, "\033[0m"); + } else { + fprintf(stream, "\033[%d;3%dm", c >> 3, c & 7); + } +#endif + } +} + +static void print_msg_module(FILE* stream, struct mp_log *log) +{ + int mod = log->legacy_mod; + int c2 = (mod + 1) % 15 + 1; + +#ifdef _WIN32 + HANDLE *wstream = stream == stderr ? hSTDERR : hSTDOUT; + if (mp_msg_docolor()) + SetConsoleTextAttribute(wstream, ansi2win32[c2&7] | FOREGROUND_INTENSITY); + fprintf(stream, "%9s", log->verbose_prefix); + if (mp_msg_docolor()) + SetConsoleTextAttribute(wstream, stdoutAttrs); +#else + if (mp_msg_docolor()) + fprintf(stream, "\033[%d;3%dm", c2 >> 3, c2 & 7); + fprintf(stream, "%9s", log->verbose_prefix); + if (mp_msg_docolor()) + fprintf(stream, "\033[0;37m"); +#endif + fprintf(stream, ": "); +} + +static void mp_msg_log_va(struct mp_log *log, int lev, const char *format, + va_list va) +{ + char tmp[MSGSIZE_MAX]; + FILE *stream = + (mp_msg_stdout_in_use || (lev == MSGL_STATUS)) ? stderr : stdout; + static int header = 1; + // indicates if last line printed was a status line + static int statusline; + + if (!mp_msg_test_log(log, lev)) return; // do not display + vsnprintf(tmp, MSGSIZE_MAX, format, va); + tmp[MSGSIZE_MAX-2] = '\n'; + tmp[MSGSIZE_MAX-1] = 0; + + /* A status line is normally intended to be overwritten by the next + * status line, and does not end with a '\n'. If we're printing a normal + * line instead after the status one print '\n' to change line. */ + if (statusline && lev != MSGL_STATUS) + fprintf(stderr, "\n"); + statusline = lev == MSGL_STATUS; + + set_msg_color(stream, lev); + if (header) { + if (mp_msg_module) { + print_msg_module(stream, log); + set_msg_color(stream, lev); + } else if (lev >= MSGL_V || verbose) { + fprintf(stream, "[%s] ", log->verbose_prefix); + } else if (log->prefix) { + fprintf(stream, "[%s] ", log->prefix); + } + } + + size_t len = strlen(tmp); + header = len && (tmp[len-1] == '\n' || tmp[len-1] == '\r'); + + fprintf(stream, "%s", tmp); + + if (mp_msg_docolor()) + { +#ifdef _WIN32 + HANDLE *wstream = lev <= MSGL_WARN ? hSTDERR : hSTDOUT; + SetConsoleTextAttribute(wstream, stdoutAttrs); +#else + fprintf(stream, "\033[0m"); +#endif + } + fflush(stream); +} + +void mp_msg_va(int mod, int lev, const char *format, va_list va) +{ + assert(initialized); + assert(mod >= 0 && mod < MSGT_MAX); + mp_msg_log_va(legacy_logs[mod], lev, format, va); +} + +void mp_msg(int mod, int lev, const char *format, ...) +{ + va_list va; + va_start(va, format); + mp_msg_va(mod, lev, format, va); + va_end(va); +} + +char *mp_gtext(const char *string) +{ +#ifdef CONFIG_GETTEXT + /* gettext expects the global locale to be set with + * setlocale(LC_ALL, ""). However doing that would suck for a + * couple of reasons (locale stuff is badly designed and sucks in + * general). + * + * First, setting the locale, especially LC_CTYPE, changes the + * behavior of various C functions and we don't want that - we + * want isalpha() for example to always behave like in the C + * locale. + + * Second, there is no way to enforce a sane character set. All + * strings inside MPlayer must always be in utf-8, not in the + * character set specified by the system locale which could be + * something different and completely insane. The locale system + * lacks any way to say "set LC_CTYPE to utf-8, ignoring the + * default system locale if it specifies something different". We + * could try to work around that flaw by leaving LC_CTYPE to the C + * locale and only setting LC_MESSAGES (which is the variable that + * must be set to tell gettext which language to translate + * to). However if we leave LC_MESSAGES set then things like + * strerror() may produce completely garbled output when they try + * to translate their results but then try to convert some + * translated non-ASCII text to the character set specified by + * LC_CTYPE which would still be in the C locale (this doesn't + * affect gettext itself because it supports specifying the + * character set directly with bind_textdomain_codeset()). + * + * So the only solution (at least short of trying to work around + * things possibly producing non-utf-8 output) is to leave all the + * locale variables unset. Note that this means it's not possible + * to get translated output from any libraries we call if they + * only rely on the broken locale system to specify the language + * to use; this is the case with libc for example. + * + * The locale changing below is rather ugly, but hard to avoid. + * gettext doesn't support specifying the translation target + * directly, only through locale. + * The main actual problem this could cause is interference with + * other threads; that could be avoided with thread-specific + * locale changes, but such functionality is less standard and I + * think it's not worth adding pre-emptively unless someone sees + * an actual problem case. + */ + setlocale(LC_MESSAGES, ""); + string = gettext(string); + setlocale(LC_MESSAGES, "C"); +#endif + return (char *)string; +} + +void mp_tmsg(int mod, int lev, const char *format, ...) +{ + va_list va; + va_start(va, format); + mp_msg_va(mod, lev, mp_gtext(format), va); + va_end(va); +} + +// legacy names +static const char *module_text[MSGT_MAX] = { + "global", + "cplayer", + "gplayer", + "vo", + "ao", + "demuxer", + "ds", + "demux", + "header", + "avsync", + "autoq", + "cfgparser", + "decaudio", + "decvideo", + "seek", + "win32", + "open", + "dvd", + "parsees", + "lirc", + "stream", + "cache", + "mencoder", + "xacodec", + "tv", + "osdep", + "spudec", + "playtree", + "input", + "vf", + "osd", + "network", + "cpudetect", + "codeccfg", + "sws", + "vobsub", + "subreader", + "af", + "netst", + "muxer", + "osdmenu", + "identify", + "radio", + "ass", + "loader", + "statusline", + "teletext", +}; + +// Create a new log context, which uses talloc_ctx as talloc parent, and parent +// as logical parent. +// The name is the prefix put before the output. It's usually prefixed by the +// parent's name. If the name starts with "/", the parent's name is not +// prefixed (except in verbose mode), and if it starts with "!", the name is +// not printed at all (except in verbose mode). +struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent, + const char *name) +{ + assert(parent); + assert(name); + struct mp_log *log = talloc_zero(talloc_ctx, struct mp_log); + log->root = parent->root; + if (name[0] == '!') { + name = &name[1]; + } else if (name[0] == '/') { + name = &name[1]; + log->prefix = talloc_strdup(log, name); + } else { + log->prefix = parent->prefix + ? talloc_asprintf(log, "%s/%s", parent->prefix, name) + : talloc_strdup(log, name); + } + log->verbose_prefix = parent->prefix + ? talloc_asprintf(log, "%s/%s", parent->prefix, name) + : talloc_strdup(log, name); + if (log->prefix && !log->prefix[0]) + log->prefix = NULL; + if (!log->verbose_prefix[0]) + log->verbose_prefix = "global"; + log->legacy_mod = parent->legacy_mod; + for (int n = 0; n < MSGT_MAX; n++) { + if (module_text[n] && strcmp(name, module_text[n]) == 0) { + log->legacy_mod = n; + break; + } + } + return log; +} + +void mp_msg_init(struct mpv_global *global) +{ + assert(!initialized); + assert(!global->log); + + struct mp_log_root *root = talloc_zero(NULL, struct mp_log_root); + root->global = global; + + struct mp_log dummy = { .root = root }; + struct mp_log *log = mp_log_new(root, &dummy, ""); + for (int n = 0; n < MSGT_MAX; n++) { + char name[80]; + snprintf(name, sizeof(name), "!%s", module_text[n]); + legacy_logs[n] = mp_log_new(root, log, name); + } + mp_msg_do_init(); + + global->log = log; + initialized = true; +} + +struct mpv_global *mp_log_get_global(struct mp_log *log) +{ + return log->root->global; +} + +void mp_msg_uninit(struct mpv_global *global) +{ + talloc_free(global->log->root); + global->log = NULL; + initialized = false; +} + +void mp_msg_log(struct mp_log *log, int lev, const char *format, ...) +{ + va_list va; + va_start(va, format); + mp_msg_log_va(log, lev, format, va); + va_end(va); +} + +void mp_tmsg_log(struct mp_log *log, int lev, const char *format, ...) +{ + va_list va; + va_start(va, format); + mp_msg_log_va(log, lev, mp_gtext(format), va); + va_end(va); +} diff --git a/mpvcore/mp_msg.h b/mpvcore/mp_msg.h new file mode 100644 index 0000000000..4685668f01 --- /dev/null +++ b/mpvcore/mp_msg.h @@ -0,0 +1,178 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_MP_MSG_H +#define MPLAYER_MP_MSG_H + +#include <stdarg.h> +#include <stdbool.h> + +struct mp_log; + +// defined in mplayer.c +extern int verbose; + +/* No-op macro to mark translated strings in the sources */ +#define _(x) x + +// verbosity elevel: + +/* Only messages level MSGL_FATAL-MSGL_STATUS should be translated, + * messages level MSGL_V and above should not be translated. */ + +#define MSGL_FATAL 0 // will exit/abort +#define MSGL_ERR 1 // continues +#define MSGL_WARN 2 // only warning +#define MSGL_HINT 3 // short help message +#define MSGL_INFO 4 // -quiet +#define MSGL_STATUS 5 // v=0 +#define MSGL_V 6 // v=1 +#define MSGL_DBG2 7 // v=2 +#define MSGL_DBG3 8 // v=3 +#define MSGL_DBG4 9 // v=4 +#define MSGL_DBG5 10 // v=5 + +#define MSGL_FIXME 1 // for conversions from printf where the appropriate MSGL is not known; set equal to ERR for obtrusiveness +#define MSGT_FIXME 0 // for conversions from printf where the appropriate MSGT is not known; set equal to GLOBAL for obtrusiveness + +// code/module: + +#define MSGT_GLOBAL 0 // common player stuff errors +#define MSGT_CPLAYER 1 // console player (mplayer.c) + +#define MSGT_VO 3 // libvo +#define MSGT_AO 4 // libao + +#define MSGT_DEMUXER 5 // demuxer.c (general stuff) +#define MSGT_DS 6 // demux stream (add/read packet etc) +#define MSGT_DEMUX 7 // fileformat-specific stuff (demux_*.c) +#define MSGT_HEADER 8 // fileformat-specific header (*header.c) + +#define MSGT_AVSYNC 9 // mplayer.c timer stuff +#define MSGT_AUTOQ 10 // mplayer.c auto-quality stuff + +#define MSGT_CFGPARSER 11 // cfgparser.c + +#define MSGT_DECAUDIO 12 // av decoder +#define MSGT_DECVIDEO 13 + +#define MSGT_SEEK 14 // seeking code +#define MSGT_WIN32 15 // win32 dll stuff +#define MSGT_OPEN 16 // open.c (stream opening) +#define MSGT_DVD 17 // open.c (DVD init/read/seek) + +#define MSGT_PARSEES 18 // parse_es.c (mpeg stream parser) +#define MSGT_LIRC 19 // lirc_mp.c and input lirc driver + +#define MSGT_STREAM 20 // stream.c +#define MSGT_CACHE 21 // cache2.c + +#define MSGT_ENCODE 22 // now encode_lavc.c + +#define MSGT_XACODEC 23 // XAnim codecs + +#define MSGT_TV 24 // TV input subsystem + +#define MSGT_OSDEP 25 // OS-dependent parts + +#define MSGT_SPUDEC 26 // spudec.c + +#define MSGT_PLAYTREE 27 // Playtree handeling (playtree.c, playtreeparser.c) + +#define MSGT_INPUT 28 + +#define MSGT_VFILTER 29 + +#define MSGT_OSD 30 + +#define MSGT_NETWORK 31 + +#define MSGT_CPUDETECT 32 + +#define MSGT_CODECCFG 33 + +#define MSGT_SWS 34 + +#define MSGT_VOBSUB 35 +#define MSGT_SUBREADER 36 + +#define MSGT_AFILTER 37 // Audio filter messages + +#define MSGT_NETST 38 // Netstream + +#define MSGT_MUXER 39 // muxer layer + +#define MSGT_IDENTIFY 41 // -identify output + +#define MSGT_RADIO 42 + +#define MSGT_ASS 43 // libass messages + +#define MSGT_LOADER 44 // dll loader messages + +#define MSGT_STATUSLINE 45 // playback/encoding status line + +#define MSGT_TELETEXT 46 // Teletext decoder + +#define MSGT_MAX 47 + +int mp_msg_test(int mod, int lev); +bool mp_msg_test_log(struct mp_log *log, int lev); + +#include "config.h" +#include "core/mp_common.h" + +char *mp_gtext(const char *string); + +void mp_msg_va(int mod, int lev, const char *format, va_list va); + +void mp_msg(int mod, int lev, const char *format, ... ) PRINTF_ATTRIBUTE(3, 4); +void mp_tmsg(int mod, int lev, const char *format, ... ) PRINTF_ATTRIBUTE(3, 4); +#define mp_dbg mp_msg + +struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent, + const char *name); + +void mp_msg_log(struct mp_log *log, int lev, const char *format, ...) + PRINTF_ATTRIBUTE(3, 4); +void mp_tmsg_log(struct mp_log *log, int lev, const char *format, ...) + PRINTF_ATTRIBUTE(3, 4); + +// Convenience macros, typically called with a pointer to a context struct +// as first argument, which has a "struct mp_log log;" member. + +#define MP_MSG(obj, lev, ...) mp_msg_log((obj)->log, lev, __VA_ARGS__) +#define MP_MSGT(obj, lev, ...) mp_msgt_log((obj)->log, lev, __VA_ARGS__) + +#define MP_FATAL(obj, ...) MP_MSG(obj, MSGL_FATAL, __VA_ARGS__) +#define MP_ERR(obj, ...) MP_MSG(obj, MSGL_ERR, __VA_ARGS__) +#define MP_WARN(obj, ...) MP_MSG(obj, MSGL_WARN, __VA_ARGS__) +#define MP_INFO(obj, ...) MP_MSG(obj, MSGL_INFO, __VA_ARGS__) +#define MP_VERBOSE(obj, ...) MP_MSG(obj, MSGL_V, __VA_ARGS__) +#define MP_DBG(obj, ...) MP_MSG(obj, MSGL_DGB2, __VA_ARGS__) +#define MP_TRACE(obj, ...) MP_MSG(obj, MSGL_DGB5, __VA_ARGS__) + +struct mpv_global; +void mp_msg_init(struct mpv_global *global); +void mp_msg_uninit(struct mpv_global *global); + +struct mpv_global *mp_log_get_global(struct mp_log *log); + +extern bool mp_msg_stdout_in_use; + +#endif /* MPLAYER_MP_MSG_H */ diff --git a/mpvcore/mp_osd.h b/mpvcore/mp_osd.h new file mode 100644 index 0000000000..0b737f0c22 --- /dev/null +++ b/mpvcore/mp_osd.h @@ -0,0 +1,52 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_MP_OSD_H +#define MPLAYER_MP_OSD_H + +#include "compat/compiler.h" + +#define OSD_MSG_TEXT 1 +#define OSD_MSG_SUB_DELAY 2 +#define OSD_MSG_SPEED 3 +#define OSD_MSG_OSD_STATUS 4 +#define OSD_MSG_BAR 5 +#define OSD_MSG_PAUSE 6 +#define OSD_MSG_RADIO_CHANNEL 7 +#define OSD_MSG_TV_CHANNEL 8 +/// Base id for messages generated from the commmand to property bridge. +#define OSD_MSG_PROPERTY 0x100 +#define OSD_MSG_SUB_BASE 0x1000 + +#define MAX_OSD_LEVEL 3 +#define MAX_TERM_OSD_LEVEL 1 +#define OSD_LEVEL_INVISIBLE 4 + +#define OSD_BAR_SEEK 256 + +struct MPContext; + +void set_osd_bar(struct MPContext *mpctx, int type,const char* name,double min,double max,double val); +void set_osd_msg(struct MPContext *mpctx, int id, int level, int time, const char* fmt, ...) PRINTF_ATTRIBUTE(5,6); +void set_osd_tmsg(struct MPContext *mpctx, int id, int level, int time, const char* fmt, ...) PRINTF_ATTRIBUTE(5,6); +void rm_osd_msg(struct MPContext *mpctx, int id); + +// osd_function is the symbol appearing in the video status, such as OSD_PLAY +void set_osd_function(struct MPContext *mpctx, int osd_function); + +#endif /* MPLAYER_MP_OSD_H */ diff --git a/mpvcore/mp_ring.c b/mpvcore/mp_ring.c new file mode 100644 index 0000000000..bd94870710 --- /dev/null +++ b/mpvcore/mp_ring.c @@ -0,0 +1,155 @@ +/* + * This file is part of mpv. + * Copyright (c) 2012 wm4 + * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com> + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <inttypes.h> +#include <libavutil/common.h> +#include <assert.h> +#include "talloc.h" +#include "core/mp_memory_barrier.h" +#include "core/mp_ring.h" + +struct mp_ring { + uint8_t *buffer; + + /* Positions of thes first readable/writeable chunks. Do not read this + * fields but use the atomic private accessors `mp_ring_get_wpos` + * and `mp_ring_get_rpos`. */ + uint32_t rpos, wpos; +}; + +static uint32_t mp_ring_get_wpos(struct mp_ring *buffer) +{ + mp_memory_barrier(); + return buffer->wpos; +} + +static uint32_t mp_ring_get_rpos(struct mp_ring *buffer) +{ + mp_memory_barrier(); + return buffer->rpos; +} + +struct mp_ring *mp_ring_new(void *talloc_ctx, int size) +{ + struct mp_ring *ringbuffer = + talloc_zero(talloc_ctx, struct mp_ring); + + *ringbuffer = (struct mp_ring) { + .buffer = talloc_size(talloc_ctx, size), + }; + + return ringbuffer; +} + +int mp_ring_drain(struct mp_ring *buffer, int len) +{ + int buffered = mp_ring_buffered(buffer); + int drain_len = FFMIN(len, buffered); + mp_atomic_add_and_fetch(&buffer->rpos, drain_len); + mp_memory_barrier(); + return drain_len; +} + +int mp_ring_read(struct mp_ring *buffer, unsigned char *dest, int len) +{ + if (!dest) return mp_ring_drain(buffer, len); + + int size = mp_ring_size(buffer); + int buffered = mp_ring_buffered(buffer); + int read_len = FFMIN(len, buffered); + int read_ptr = mp_ring_get_rpos(buffer) % size; + + int len1 = FFMIN(size - read_ptr, read_len); + int len2 = read_len - len1; + + memcpy(dest, buffer->buffer + read_ptr, len1); + memcpy(dest + len1, buffer->buffer, len2); + + mp_atomic_add_and_fetch(&buffer->rpos, read_len); + mp_memory_barrier(); + + return read_len; +} + +int mp_ring_read_cb(struct mp_ring *buffer, void *ctx, int len, + void (*func)(void*, void*, int)) +{ + // The point of this function is defining custom read behaviour, assume + // it's a programmers error if func is null. + assert(func); + + int size = mp_ring_size(buffer); + int buffered = mp_ring_buffered(buffer); + int read_len = FFMIN(len, buffered); + int read_ptr = mp_ring_get_rpos(buffer) % size; + + func(ctx, buffer->buffer + read_ptr, read_len); + + return mp_ring_drain(buffer, read_len); +} + +int mp_ring_write(struct mp_ring *buffer, unsigned char *src, int len) +{ + int size = mp_ring_size(buffer); + int free = mp_ring_available(buffer); + int write_len = FFMIN(len, free); + int write_ptr = mp_ring_get_wpos(buffer) % size; + + int len1 = FFMIN(size - write_ptr, write_len); + int len2 = write_len - len1; + + memcpy(buffer->buffer + write_ptr, src, len1); + memcpy(buffer->buffer, src + len1, len2); + + mp_atomic_add_and_fetch(&buffer->wpos, write_len); + mp_memory_barrier(); + + return write_len; +} + +void mp_ring_reset(struct mp_ring *buffer) +{ + buffer->wpos = buffer->rpos = 0; + mp_memory_barrier(); +} + +int mp_ring_available(struct mp_ring *buffer) +{ + return mp_ring_size(buffer) - mp_ring_buffered(buffer); +} + +int mp_ring_size(struct mp_ring *buffer) +{ + return talloc_get_size(buffer->buffer); +} + +int mp_ring_buffered(struct mp_ring *buffer) +{ + return (mp_ring_get_wpos(buffer) - mp_ring_get_rpos(buffer)); +} + +char *mp_ring_repr(struct mp_ring *buffer, void *talloc_ctx) +{ + return talloc_asprintf( + talloc_ctx, + "Ringbuffer { .size = %dB, .buffered = %dB, .available = %dB }", + mp_ring_size(buffer), + mp_ring_buffered(buffer), + mp_ring_available(buffer)); +} diff --git a/mpvcore/mp_ring.h b/mpvcore/mp_ring.h new file mode 100644 index 0000000000..ba104af625 --- /dev/null +++ b/mpvcore/mp_ring.h @@ -0,0 +1,130 @@ +/* + * This file is part of mpv. + * Copyright (c) 2012 wm4 + * Copyright (c) 2013 Stefano Pigozzi <stefano.pigozzi@gmail.com> + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPV_MP_RING_H +#define MPV_MP_RING_H + +/** + * A simple non-blocking SPSC (single producer, single consumer) ringbuffer + * implementation. Thread safety is accomplished through atomic operations. + */ + +struct mp_ring; + +/** + * Instantiate a new ringbuffer + * + * talloc_ctx: talloc context of the newly created object + * size: total size in bytes + * return: the newly created ringbuffer + */ +struct mp_ring *mp_ring_new(void *talloc_ctx, int size); + +/** + * Read data from the ringbuffer + * + * buffer: target ringbuffer instance + * dest: destination buffer for the read data. If NULL read data is discarded. + * len: maximum number of bytes to read + * return: number of bytes read + */ +int mp_ring_read(struct mp_ring *buffer, unsigned char *dest, int len); + +/** + * Read data from the ringbuffer + * + * This function behaves similarly to `av_fifo_generic_read` and was actually + * added for compatibility with code that was written for it. + * This function will drain the returned amount of bytes from the ringbuffer + * so you don't have to handle that in inside `func`. + * + * buffer: target ringbuffer instance + * ctx: context for the callback function + * len: maximum number of bytes to read + * func: callback function to customize reading behaviour. It will be called + * by `mp_ring_read_cb` with the following parameters: + * ctx: context data provided to `mp_ring_read_cb` + * src: source buffer to read from + * len: the *exact* amount of bytes to read. These will be drained + * by the ring after this callback is called. + * return: number of bytes read + */ +int mp_ring_read_cb(struct mp_ring *buffer, void *ctx, int len, + void (*func)(void *ctx, void *src, int len)); + +/** + * Write data to the ringbuffer + * + * buffer: target ringbuffer instance + * src: source buffer for the write data + * len: maximum number of bytes to write + * return: number of bytes written + */ +int mp_ring_write(struct mp_ring *buffer, unsigned char *src, int len); + +/** + * Drain data from the ringbuffer + * + * buffer: target ringbuffer instance + * len: maximum number of bytes to drain + * return: number of bytes drained + */ +int mp_ring_drain(struct mp_ring *buffer, int len); + +/** + * Reset the ringbuffer discarding any content + * + * buffer: target ringbuffer instance + */ +void mp_ring_reset(struct mp_ring *buffer); + +/** + * Get the available size for writing + * + * buffer: target ringbuffer instance + * return: number of bytes that can be written + */ +int mp_ring_available(struct mp_ring *buffer); + +/** + * Get the total size + * + * buffer: target ringbuffer instance + * return: total ringbuffer size in bytes + */ +int mp_ring_size(struct mp_ring *buffer); + +/** + * Get the available size for reading + * + * buffer: target ringbuffer instance + * return: number of bytes ready for reading + */ +int mp_ring_buffered(struct mp_ring *buffer); + +/** + * Get a string representation of the ringbuffer + * + * buffer: target ringbuffer instance + * talloc_ctx: talloc context of the newly created string + * return: string representing the ringbuffer + */ +char *mp_ring_repr(struct mp_ring *buffer, void *talloc_ctx); + +#endif diff --git a/mpvcore/mp_talloc.h b/mpvcore/mp_talloc.h new file mode 100644 index 0000000000..1dcb0bce07 --- /dev/null +++ b/mpvcore/mp_talloc.h @@ -0,0 +1,61 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPV_TALLOC_H +#define MPV_TALLOC_H + +#include "talloc.h" +#include "compat/compiler.h" + +#define MP_TALLOC_ELEMS(p) (talloc_get_size(p) / sizeof((p)[0])) +#define MP_GROW_ARRAY(p, nextidx) do { \ + if ((nextidx) == MP_TALLOC_ELEMS(p)) \ + (p) = talloc_realloc_size(NULL, p, talloc_get_size(p) * 2); } while (0) +#define MP_RESIZE_ARRAY(ctx, p, count) do { \ + (p) = talloc_realloc_size((ctx), p, (count) * sizeof((p)[0])); } while (0) + +#define MP_TARRAY_GROW(ctx, p, nextidx) \ + do { \ + size_t nextidx_ = (nextidx); \ + size_t nelems_ = MP_TALLOC_ELEMS(p); \ + if (nextidx_ >= nelems_) \ + (p) = talloc_realloc_size(ctx, p, \ + (nextidx_ + 1) * sizeof((p)[0]) * 2);\ + } while (0) + +#define MP_TARRAY_APPEND(ctx, p, idxvar, ...) \ + do { \ + MP_TARRAY_GROW(ctx, p, idxvar); \ + (p)[(idxvar)] = (MP_EXPAND_ARGS(__VA_ARGS__));\ + (idxvar)++; \ + } while (0) + +// Doesn't actually free any memory, or do any other talloc calls. +#define MP_TARRAY_REMOVE_AT(p, idxvar, at) \ + do { \ + size_t at_ = (at); \ + assert(at_ <= (idxvar)); \ + memmove((p) + at_, (p) + at_ + 1, \ + ((idxvar) - at_ - 1) * sizeof((p)[0])); \ + (idxvar)--; \ + } while (0) + +#define talloc_struct(ctx, type, ...) \ + talloc_memdup(ctx, &(type) MP_EXPAND_ARGS(__VA_ARGS__), sizeof(type)) + +#endif diff --git a/mpvcore/mplayer.c b/mpvcore/mplayer.c new file mode 100644 index 0000000000..d3872442ee --- /dev/null +++ b/mpvcore/mplayer.c @@ -0,0 +1,4747 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdio.h> +#include <stdlib.h> +#include <stdbool.h> +#include <math.h> +#include <assert.h> +#include <ctype.h> + +#ifdef PTW32_STATIC_LIB +#include <pthread.h> +#endif + +#include <libavutil/intreadwrite.h> +#include <libavutil/attributes.h> +#include <libavutil/md5.h> +#include <libavutil/common.h> + +#include <libavcodec/version.h> + +#include "config.h" +#include "talloc.h" + +#include "osdep/io.h" + +#if defined(__MINGW32__) || defined(__CYGWIN__) +#include <windows.h> +#endif +#define WAKEUP_PERIOD 0.5 +#include <string.h> +#include <unistd.h> + +// #include <sys/mman.h> +#include <sys/types.h> +#ifndef __MINGW32__ +#include <sys/ioctl.h> +#include <sys/wait.h> +#endif + +#include <sys/time.h> +#include <sys/stat.h> + +#include <signal.h> +#include <time.h> +#include <fcntl.h> +#include <limits.h> + +#include <errno.h> + +#include "core/mpv_global.h" +#include "core/mp_msg.h" +#include "av_log.h" + + +#include "core/m_option.h" +#include "core/m_config.h" +#include "core/resolve.h" +#include "core/m_property.h" + +#include "sub/find_subfiles.h" +#include "sub/dec_sub.h" +#include "sub/sd.h" + +#include "core/mp_osd.h" +#include "video/out/vo.h" +#include "core/screenshot.h" + +#include "sub/sub.h" +#include "core/cpudetect.h" + +#ifdef CONFIG_X11 +#include "video/out/x11_common.h" +#endif + +#ifdef CONFIG_COCOA +#include "osdep/macosx_application.h" +#endif + +#include "audio/out/ao.h" + +#include "core/codecs.h" + +#include "osdep/getch2.h" +#include "osdep/timer.h" + +#include "core/input/input.h" +#include "core/encode.h" + +#include "osdep/priority.h" + +#include "stream/tv.h" +#include "stream/stream_radio.h" +#ifdef CONFIG_DVBIN +#include "stream/dvbin.h" +#endif + +//**************************************************************************// +// Playtree +//**************************************************************************// +#include "core/playlist.h" +#include "core/playlist_parser.h" + +//**************************************************************************// +// Config +//**************************************************************************// +#include "core/parser-cfg.h" +#include "core/parser-mpcmd.h" + +//**************************************************************************// +// Config file +//**************************************************************************// + +#include "core/path.h" + +//**************************************************************************// +//**************************************************************************// +// Input media streaming & demultiplexer: +//**************************************************************************// + +#include "stream/stream.h" +#include "demux/demux.h" +#include "demux/stheader.h" + +#include "audio/filter/af.h" +#include "audio/decode/dec_audio.h" +#include "video/decode/dec_video.h" +#include "video/mp_image.h" +#include "video/filter/vf.h" +#include "video/decode/vd.h" + +#include "audio/mixer.h" + +#include "core/mp_core.h" +#include "core/options.h" + +const char mp_help_text[] = _( +"Usage: mpv [options] [url|path/]filename\n" +"\n" +"Basic options: (complete list in the man page)\n" +" --start=<time> seek to given (percent, seconds, or hh:mm:ss) position\n" +" --no-audio do not play sound\n" +" --no-video do not play video\n" +" --fs fullscreen playback\n" +" --sub=<file> specify subtitle file to use\n" +" --playlist=<file> specify playlist file\n" +"\n"); + +static const char av_desync_help_text[] = _( +"\n\n" +" *************************************************\n" +" **** Audio/Video desynchronisation detected! ****\n" +" *************************************************\n\n" +"This means either the audio or the video is played too slowly.\n" +"Possible reasons, problems, workarounds:\n" +"- Your system is simply too slow for this file.\n" +" Transcode it to a lower bitrate file with tools like HandBrake.\n" +"- Broken/buggy _audio_ driver.\n" +" Experiment with different values for --autosync, 30 is a good start.\n" +" If you have PulseAudio, try --ao=alsa .\n" +"- Slow video output.\n" +" Try a different -vo driver (-vo help for a list) or try -framedrop!\n" +"- Playing a video file with --vo=opengl with higher FPS than the monitor.\n" +" This is due to vsync limiting the framerate.\n" +"- Playing from a slow network source.\n" +" Download the file instead.\n" +"- Try to find out whether audio or video is causing this by experimenting\n" +" with --no-video and --no-audio.\n" +"If none of this helps you, file a bug report.\n\n"); + + +//**************************************************************************// +//**************************************************************************// + +#include "sub/ass_mp.h" + + +// --- + +#include "core/mp_common.h" +#include "core/command.h" + +static void reset_subtitles(struct MPContext *mpctx); +static void reinit_subs(struct MPContext *mpctx); + +static double get_relative_time(struct MPContext *mpctx) +{ + int64_t new_time = mp_time_us(); + int64_t delta = new_time - mpctx->last_time; + mpctx->last_time = new_time; + return delta * 0.000001; +} + +static double rel_time_to_abs(struct MPContext *mpctx, struct m_rel_time t, + double fallback_time) +{ + double length = get_time_length(mpctx); + switch (t.type) { + case REL_TIME_ABSOLUTE: + return t.pos; + case REL_TIME_NEGATIVE: + if (length != 0) + return FFMAX(length - t.pos, 0.0); + break; + case REL_TIME_PERCENT: + if (length != 0) + return length * (t.pos / 100.0); + break; + case REL_TIME_CHAPTER: + if (chapter_start_time(mpctx, t.pos) >= 0) + return chapter_start_time(mpctx, t.pos); + break; + } + return fallback_time; +} + +static double get_play_end_pts(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + if (opts->play_end.type) { + return rel_time_to_abs(mpctx, opts->play_end, MP_NOPTS_VALUE); + } else if (opts->play_length.type) { + double start = rel_time_to_abs(mpctx, opts->play_start, 0); + double length = rel_time_to_abs(mpctx, opts->play_length, -1); + if (start != -1 && length != -1) + return start + length; + } + return MP_NOPTS_VALUE; +} + +static void print_stream(struct MPContext *mpctx, struct track *t) +{ + struct sh_stream *s = t->stream; + const char *tname = "?"; + const char *selopt = "?"; + const char *langopt = "?"; + const char *iid = NULL; + switch (t->type) { + case STREAM_VIDEO: + tname = "Video"; selopt = "vid"; langopt = NULL; iid = "VID"; + break; + case STREAM_AUDIO: + tname = "Audio"; selopt = "aid"; langopt = "alang"; iid = "AID"; + break; + case STREAM_SUB: + tname = "Subs"; selopt = "sid"; langopt = "slang"; iid = "SID"; + break; + } + mp_msg(MSGT_CPLAYER, MSGL_INFO, "[stream] %-5s %3s", + tname, mpctx->current_track[t->type] == t ? "(+)" : ""); + mp_msg(MSGT_CPLAYER, MSGL_INFO, " --%s=%d", selopt, t->user_tid); + if (t->lang && langopt) + mp_msg(MSGT_CPLAYER, MSGL_INFO, " --%s=%s", langopt, t->lang); + if (t->default_track) + mp_msg(MSGT_CPLAYER, MSGL_INFO, " (*)"); + if (t->attached_picture) + mp_msg(MSGT_CPLAYER, MSGL_INFO, " [P]"); + if (t->title) + mp_msg(MSGT_CPLAYER, MSGL_INFO, " '%s'", t->title); + const char *codec = s ? s->codec : NULL; + mp_msg(MSGT_CPLAYER, MSGL_INFO, " (%s)", codec ? codec : "<unknown>"); + if (t->is_external) + mp_msg(MSGT_CPLAYER, MSGL_INFO, " (external)"); + mp_msg(MSGT_CPLAYER, MSGL_INFO, "\n"); + // legacy compatibility + if (!iid) + return; + int id = t->user_tid; + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_%s_ID=%d\n", iid, id); + if (t->title) + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_%s_%d_NAME=%s\n", iid, id, t->title); + if (t->lang) + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_%s_%d_LANG=%s\n", iid, id, t->lang); +} + +static void print_file_properties(struct MPContext *mpctx, const char *filename) +{ + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_FILENAME=%s\n", + filename); + if (mpctx->sh_video) { + /* Assume FOURCC if all bytes >= 0x20 (' ') */ + if (mpctx->sh_video->format >= 0x20202020) + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_FORMAT=%.4s\n", (char *)&mpctx->sh_video->format); + else + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_FORMAT=0x%08X\n", mpctx->sh_video->format); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_BITRATE=%d\n", mpctx->sh_video->i_bps * 8); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_WIDTH=%d\n", mpctx->sh_video->disp_w); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_HEIGHT=%d\n", mpctx->sh_video->disp_h); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_FPS=%5.3f\n", mpctx->sh_video->fps); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_VIDEO_ASPECT=%1.4f\n", mpctx->sh_video->aspect); + } + if (mpctx->sh_audio) { + /* Assume FOURCC if all bytes >= 0x20 (' ') */ + if (mpctx->sh_audio->format >= 0x20202020) + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_AUDIO_FORMAT=%.4s\n", (char *)&mpctx->sh_audio->format); + else + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_AUDIO_FORMAT=%d\n", mpctx->sh_audio->format); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_AUDIO_BITRATE=%d\n", mpctx->sh_audio->i_bps * 8); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_AUDIO_RATE=%d\n", mpctx->sh_audio->samplerate); + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_AUDIO_NCH=%d\n", mpctx->sh_audio->channels.num); + } + mp_msg(MSGT_IDENTIFY, MSGL_INFO, + "ID_LENGTH=%.2f\n", get_time_length(mpctx)); + int chapter_count = get_chapter_count(mpctx); + if (chapter_count >= 0) { + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTERS=%d\n", chapter_count); + for (int i = 0; i < chapter_count; i++) { + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTER_ID=%d\n", i); + // print in milliseconds + double time = chapter_start_time(mpctx, i) * 1000.0; + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTER_%d_START=%"PRId64"\n", + i, (int64_t)(time < 0 ? -1 : time)); + char *name = chapter_name(mpctx, i); + if (name) { + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_CHAPTER_%d_NAME=%s\n", i, + name); + talloc_free(name); + } + } + } + struct demuxer *demuxer = mpctx->master_demuxer; + if (demuxer->num_editions > 1) + mp_msg(MSGT_CPLAYER, MSGL_INFO, + "Playing edition %d of %d (--edition=%d).\n", + demuxer->edition + 1, demuxer->num_editions, demuxer->edition); + for (int t = 0; t < STREAM_TYPE_COUNT; t++) { + for (int n = 0; n < mpctx->num_tracks; n++) + if (mpctx->tracks[n]->type == t) + print_stream(mpctx, mpctx->tracks[n]); + } +} + +// Time used to seek external tracks to. +static double get_main_demux_pts(struct MPContext *mpctx) +{ + double main_new_pos = MP_NOPTS_VALUE; + if (mpctx->demuxer) { + for (int n = 0; n < mpctx->demuxer->num_streams; n++) { + if (main_new_pos == MP_NOPTS_VALUE) + main_new_pos = demux_get_next_pts(mpctx->demuxer->streams[n]); + } + } + return main_new_pos; +} + +static void set_demux_field(struct MPContext *mpctx, enum stream_type type, + struct sh_stream *s) +{ + mpctx->sh[type] = s; + // redundant fields for convenience access + switch(type) { + case STREAM_VIDEO: mpctx->sh_video = s ? s->video : NULL; break; + case STREAM_AUDIO: mpctx->sh_audio = s ? s->audio : NULL; break; + case STREAM_SUB: mpctx->sh_sub = s ? s->sub : NULL; break; + } +} + +static void init_demux_stream(struct MPContext *mpctx, enum stream_type type) +{ + struct track *track = mpctx->current_track[type]; + set_demux_field(mpctx, type, track ? track->stream : NULL); + struct sh_stream *stream = mpctx->sh[type]; + if (stream) { + demuxer_switch_track(stream->demuxer, type, stream); + if (track->is_external) { + double pts = get_main_demux_pts(mpctx); + demux_seek(stream->demuxer, pts, mpctx->audio_delay, SEEK_ABSOLUTE); + } + } +} + +static void cleanup_demux_stream(struct MPContext *mpctx, enum stream_type type) +{ + struct sh_stream *stream = mpctx->sh[type]; + if (stream) + demuxer_switch_track(stream->demuxer, type, NULL); + set_demux_field(mpctx, type, NULL); +} + +// Switch the demuxers to current track selection. This is possibly important +// for intialization: if something reads packets from the demuxer (like at least +// reinit_audio_chain does, or when seeking), packets from the other streams +// should be queued instead of discarded. So all streams should be enabled +// before the first initialization function is called. +static void preselect_demux_streams(struct MPContext *mpctx) +{ + // Disable all streams, just to be sure no unwanted streams are selected. + for (int n = 0; n < mpctx->num_sources; n++) { + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + struct track *track = mpctx->current_track[type]; + if (!(track && track->demuxer == mpctx->sources[n] && + demuxer_stream_is_selected(track->demuxer, track->stream))) + demuxer_switch_track(mpctx->sources[n], type, NULL); + } + } + + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + struct track *track = mpctx->current_track[type]; + if (track && track->stream) + demuxer_switch_track(track->stream->demuxer, type, track->stream); + } +} + +static void uninit_subs(struct demuxer *demuxer) +{ + for (int i = 0; i < demuxer->num_streams; i++) { + struct sh_stream *sh = demuxer->streams[i]; + if (sh->sub) { + sub_destroy(sh->sub->dec_sub); + sh->sub->dec_sub = NULL; + } + } +} + +void uninit_player(struct MPContext *mpctx, unsigned int mask) +{ + struct MPOpts *opts = mpctx->opts; + + mask &= mpctx->initialized_flags; + + mp_msg(MSGT_CPLAYER, MSGL_DBG2, "\n*** uninit(0x%X)\n", mask); + + if (mask & INITIALIZED_ACODEC) { + mpctx->initialized_flags &= ~INITIALIZED_ACODEC; + if (mpctx->sh_audio) + uninit_audio(mpctx->sh_audio); + cleanup_demux_stream(mpctx, STREAM_AUDIO); + mpctx->mixer.afilter = NULL; + } + + if (mask & INITIALIZED_SUB) { + mpctx->initialized_flags &= ~INITIALIZED_SUB; + if (mpctx->sh_sub) + sub_reset(mpctx->sh_sub->dec_sub); + cleanup_demux_stream(mpctx, STREAM_SUB); + mpctx->osd->dec_sub = NULL; + reset_subtitles(mpctx); + } + + if (mask & INITIALIZED_VCODEC) { + mpctx->initialized_flags &= ~INITIALIZED_VCODEC; + if (mpctx->sh_video) + uninit_video(mpctx->sh_video); + cleanup_demux_stream(mpctx, STREAM_VIDEO); + mpctx->sync_audio_to_video = false; + } + + if (mask & INITIALIZED_DEMUXER) { + mpctx->initialized_flags &= ~INITIALIZED_DEMUXER; + for (int i = 0; i < mpctx->num_tracks; i++) { + talloc_free(mpctx->tracks[i]); + } + mpctx->num_tracks = 0; + for (int t = 0; t < STREAM_TYPE_COUNT; t++) + mpctx->current_track[t] = NULL; + assert(!mpctx->sh_video && !mpctx->sh_audio && !mpctx->sh_sub); + mpctx->master_demuxer = NULL; + for (int i = 0; i < mpctx->num_sources; i++) { + uninit_subs(mpctx->sources[i]); + struct demuxer *demuxer = mpctx->sources[i]; + if (demuxer->stream != mpctx->stream) + free_stream(demuxer->stream); + free_demuxer(demuxer); + } + talloc_free(mpctx->sources); + mpctx->sources = NULL; + mpctx->demuxer = NULL; + mpctx->num_sources = 0; + talloc_free(mpctx->timeline); + mpctx->timeline = NULL; + mpctx->num_timeline_parts = 0; + talloc_free(mpctx->chapters); + mpctx->chapters = NULL; + mpctx->num_chapters = 0; + mpctx->video_offset = 0; + } + + // kill the cache process: + if (mask & INITIALIZED_STREAM) { + mpctx->initialized_flags &= ~INITIALIZED_STREAM; + if (mpctx->stream) + free_stream(mpctx->stream); + mpctx->stream = NULL; + } + + if (mask & INITIALIZED_VO) { + mpctx->initialized_flags &= ~INITIALIZED_VO; + vo_destroy(mpctx->video_out); + mpctx->video_out = NULL; + } + + // Must be after libvo uninit, as few vo drivers (svgalib) have tty code. + if (mask & INITIALIZED_GETCH2) { + mpctx->initialized_flags &= ~INITIALIZED_GETCH2; + mp_msg(MSGT_CPLAYER, MSGL_DBG2, "\n[[[uninit getch2]]]\n"); + // restore terminal: + getch2_disable(); + } + + if (mask & INITIALIZED_VOL) { + mpctx->initialized_flags &= ~INITIALIZED_VOL; + if (mpctx->mixer.ao) { + // Normally the mixer remembers volume, but do it even if the + // volume is set explicitly with --volume=... (so that the same + // volume is restored on reinit) + if (opts->mixer_init_volume >= 0 && mpctx->mixer.user_set_volume) + mixer_getbothvolume(&mpctx->mixer, &opts->mixer_init_volume); + if (opts->mixer_init_mute >= 0 && mpctx->mixer.user_set_mute) + opts->mixer_init_mute = mixer_getmute(&mpctx->mixer); + } + } + + if (mask & INITIALIZED_AO) { + mpctx->initialized_flags &= ~INITIALIZED_AO; + if (mpctx->mixer.ao) + mixer_uninit(&mpctx->mixer); + mpctx->mixer.ao = NULL; + if (mpctx->ao) + ao_uninit(mpctx->ao, mpctx->stop_play != AT_END_OF_FILE); + mpctx->ao = NULL; + } +} + +static MP_NORETURN void exit_player(struct MPContext *mpctx, + enum exit_reason how) +{ + int rc; + uninit_player(mpctx, INITIALIZED_ALL); + +#ifdef CONFIG_ENCODING + encode_lavc_finish(mpctx->encode_lavc_ctx); + encode_lavc_free(mpctx->encode_lavc_ctx); +#endif + + mpctx->encode_lavc_ctx = NULL; + +#if defined(__MINGW32__) || defined(__CYGWIN__) + timeEndPeriod(1); +#endif + + mp_input_uninit(mpctx->input); + + osd_free(mpctx->osd); + +#ifdef CONFIG_ASS + ass_library_done(mpctx->ass_library); + mpctx->ass_library = NULL; +#endif + + if (how != EXIT_NONE) { + const char *reason; + switch (how) { + case EXIT_SOMENOTPLAYED: + case EXIT_PLAYED: + reason = "End of file"; + break; + case EXIT_NOTPLAYED: + reason = "No files played"; + break; + case EXIT_ERROR: + reason = "Fatal error"; + break; + default: + reason = "Quit"; + } + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, "\nExiting... (%s)\n", reason); + } + + if (mpctx->has_quit_custom_rc) { + rc = mpctx->quit_custom_rc; + } else { + switch (how) { + case EXIT_ERROR: + rc = 1; break; + case EXIT_NOTPLAYED: + rc = 2; break; + case EXIT_SOMENOTPLAYED: + rc = 3; break; + default: + rc = 0; + } + } + + // must be last since e.g. mp_msg uses option values + // that will be freed by this. + + mp_msg_uninit(mpctx->global); + talloc_free(mpctx); + +#ifdef CONFIG_COCOA + terminate_cocoa_application(); + // never reach here: + // terminate calls exit itself, just silence compiler warning + exit(0); +#else + exit(rc); +#endif +} + +static void mk_config_dir(char *subdir) +{ + void *tmp = talloc_new(NULL); + char *confdir = talloc_steal(tmp, mp_find_user_config_file("")); + if (subdir) + confdir = mp_path_join(tmp, bstr0(confdir), bstr0(subdir)); + mkdir(confdir, 0777); + talloc_free(tmp); +} + +static int cfg_include(struct m_config *conf, char *filename, int flags) +{ + return m_config_parse_config_file(conf, filename, flags); +} + +#define DEF_CONFIG "# Write your default config options here!\n\n\n" + +static bool parse_cfgfiles(struct MPContext *mpctx, m_config_t *conf) +{ + struct MPOpts *opts = mpctx->opts; + char *conffile; + int conffile_fd; + if (!opts->load_config) + return true; + if (!m_config_parse_config_file(conf, MPLAYER_CONFDIR "/mpv.conf", 0) < 0) + return false; + mk_config_dir(NULL); + if ((conffile = mp_find_user_config_file("config")) == NULL) + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, + "mp_find_user_config_file(\"config\") problem\n"); + else { + if ((conffile_fd = open(conffile, O_CREAT | O_EXCL | O_WRONLY, + 0666)) != -1) { + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, + "Creating config file: %s\n", conffile); + write(conffile_fd, DEF_CONFIG, sizeof(DEF_CONFIG) - 1); + close(conffile_fd); + } + if (m_config_parse_config_file(conf, conffile, 0) < 0) + return false; + talloc_free(conffile); + } + return true; +} + +#define PROFILE_CFG_PROTOCOL "protocol." + +static void load_per_protocol_config(m_config_t *conf, const char * const file) +{ + char *str; + char protocol[strlen(PROFILE_CFG_PROTOCOL) + strlen(file) + 1]; + m_profile_t *p; + + /* does filename actually uses a protocol ? */ + str = strstr(file, "://"); + if (!str) + return; + + sprintf(protocol, "%s%s", PROFILE_CFG_PROTOCOL, file); + protocol[strlen(PROFILE_CFG_PROTOCOL) + strlen(file) - strlen(str)] = '\0'; + p = m_config_get_profile0(conf, protocol); + if (p) { + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, + "Loading protocol-related profile '%s'\n", protocol); + m_config_set_profile(conf, p, M_SETOPT_BACKUP); + } +} + +#define PROFILE_CFG_EXTENSION "extension." + +static void load_per_extension_config(m_config_t *conf, const char * const file) +{ + char *str; + char extension[strlen(PROFILE_CFG_EXTENSION) + 8]; + m_profile_t *p; + + /* does filename actually have an extension ? */ + str = strrchr(file, '.'); + if (!str) + return; + + sprintf(extension, PROFILE_CFG_EXTENSION); + strncat(extension, ++str, 7); + p = m_config_get_profile0(conf, extension); + if (p) { + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, + "Loading extension-related profile '%s'\n", extension); + m_config_set_profile(conf, p, M_SETOPT_BACKUP); + } +} + +#define PROFILE_CFG_VO "vo." +#define PROFILE_CFG_AO "ao." + +static void load_per_output_config(m_config_t *conf, char *cfg, char *out) +{ + char profile[strlen(cfg) + strlen(out) + 1]; + m_profile_t *p; + + if (!out && !out[0]) + return; + + sprintf(profile, "%s%s", cfg, out); + p = m_config_get_profile0(conf, profile); + if (p) { + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, + "Loading extension-related profile '%s'\n", profile); + m_config_set_profile(conf, p, M_SETOPT_BACKUP); + } +} + +/** + * Tries to load a config file (in file local mode) + * @return 0 if file was not found, 1 otherwise + */ +static int try_load_config(m_config_t *conf, const char *file) +{ + if (!mp_path_exists(file)) + return 0; + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, "Loading config '%s'\n", file); + m_config_parse_config_file(conf, file, M_SETOPT_BACKUP); + return 1; +} + +static void load_per_file_config(m_config_t *conf, const char * const file, + bool search_file_dir) +{ + char *confpath; + char cfg[MP_PATH_MAX]; + const char *name; + + if (strlen(file) > MP_PATH_MAX - 14) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "Filename is too long, " + "can not load file or directory specific config files\n"); + return; + } + sprintf(cfg, "%s.conf", file); + + name = mp_basename(cfg); + if (search_file_dir) { + char dircfg[MP_PATH_MAX]; + strcpy(dircfg, cfg); + strcpy(dircfg + (name - cfg), "mpv.conf"); + try_load_config(conf, dircfg); + + if (try_load_config(conf, cfg)) + return; + } + + if ((confpath = mp_find_user_config_file(name)) != NULL) { + try_load_config(conf, confpath); + + talloc_free(confpath); + } +} + +static bool might_be_an_url(bstr f) +{ + return bstr_find0(f, "://") >= 0; +} + +#define MP_WATCH_LATER_CONF "watch_later" + +static char *get_playback_resume_config_filename(const char *fname) +{ + char *res = NULL; + void *tmp = talloc_new(NULL); + const char *realpath = fname; + if (!might_be_an_url(bstr0(fname))) { + char *cwd = mp_getcwd(tmp); + if (!cwd) + goto exit; + realpath = mp_path_join(tmp, bstr0(cwd), bstr0(fname)); + } + uint8_t md5[16]; + av_md5_sum(md5, realpath, strlen(realpath)); + char *conf = talloc_strdup(tmp, ""); + for (int i = 0; i < 16; i++) + conf = talloc_asprintf_append(conf, "%02X", md5[i]); + + conf = talloc_asprintf(tmp, "%s/%s", MP_WATCH_LATER_CONF, conf); + + res = mp_find_user_config_file(conf); + +exit: + talloc_free(tmp); + return res; +} + +static const char *backup_properties[] = { + "osd-level", + //"loop", + "speed", + "edition", + "pause", + //"volume", + //"mute", + "audio-delay", + //"balance", + "fullscreen", + "colormatrix", + "colormatrix-input-range", + "colormatrix-output-range", + "ontop", + "border", + "gamma", + "brightness", + "contrast", + "saturation", + "hue", + "panscan", + "aid", + "vid", + "sid", + "sub-delay", + "sub-pos", + "sub-visibility", + "sub-scale", + "ass-use-margins", + "ass-vsfilter-aspect-compat", + "ass-style-override", + 0 +}; + +void mp_write_watch_later_conf(struct MPContext *mpctx) +{ + void *tmp = talloc_new(NULL); + char *filename = mpctx->filename; + if (!filename) + goto exit; + + double pos = get_current_time(mpctx); + int percent = get_percent_pos(mpctx); + if (percent < 1 || percent > 99 || pos == MP_NOPTS_VALUE) + goto exit; + + mk_config_dir(MP_WATCH_LATER_CONF); + + char *conffile = get_playback_resume_config_filename(mpctx->filename); + talloc_steal(tmp, conffile); + if (!conffile) + goto exit; + + FILE *file = fopen(conffile, "wb"); + if (!file) + goto exit; + fprintf(file, "start=%f\n", pos); + for (int i = 0; backup_properties[i]; i++) { + const char *pname = backup_properties[i]; + char *val = NULL; + int r = mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx); + if (r == M_PROPERTY_OK) + fprintf(file, "%s=%s\n", pname, val); + talloc_free(val); + } + fclose(file); + +exit: + talloc_free(tmp); +} + +static void load_playback_resume(m_config_t *conf, const char *file) +{ + char *fname = get_playback_resume_config_filename(file); + if (fname) { + try_load_config(conf, fname); + unlink(fname); + } + talloc_free(fname); +} + +static void load_per_file_options(m_config_t *conf, + struct playlist_param *params, + int params_count) +{ + for (int n = 0; n < params_count; n++) { + m_config_set_option_ext(conf, params[n].name, params[n].value, + M_SETOPT_BACKUP); + } +} + +/* When demux performs a blocking operation (network connection or + * cache filling) if the operation fails we use this function to check + * if it was interrupted by the user. + * The function returns whether it was interrupted. */ +static bool demux_was_interrupted(struct MPContext *mpctx) +{ + for (;;) { + if (mpctx->stop_play != KEEP_PLAYING + && mpctx->stop_play != AT_END_OF_FILE) + return true; + mp_cmd_t *cmd = mp_input_get_cmd(mpctx->input, 0, 0); + if (!cmd) + break; + if (mp_input_is_abort_cmd(cmd->id)) + run_command(mpctx, cmd); + mp_cmd_free(cmd); + } + return false; +} + +static int find_new_tid(struct MPContext *mpctx, enum stream_type t) +{ + int new_id = -1; + for (int i = 0; i < mpctx->num_tracks; i++) { + struct track *track = mpctx->tracks[i]; + if (track->type == t) + new_id = FFMAX(new_id, track->user_tid); + } + return new_id + 1; +} + +// Map stream number (as used by libdvdread) to MPEG IDs (as used by demuxer). +static int map_id_from_demuxer(struct demuxer *d, enum stream_type type, int id) +{ + if (d->stream->uncached_type == STREAMTYPE_DVD && type == STREAM_SUB) + id = id & 0x1F; + return id; +} + +static struct track *add_stream_track(struct MPContext *mpctx, + struct sh_stream *stream, + bool under_timeline) +{ + for (int i = 0; i < mpctx->num_tracks; i++) { + struct track *track = mpctx->tracks[i]; + if (track->stream == stream) + return track; + // DVD subtitle track that was added later + if (stream->type == STREAM_SUB && track->type == STREAM_SUB && + map_id_from_demuxer(stream->demuxer, stream->type, + stream->demuxer_id) == track->demuxer_id + && !track->stream) + { + track->stream = stream; + track->demuxer_id = stream->demuxer_id; + // Initialize lazily selected track + bool selected = track == mpctx->current_track[STREAM_SUB]; + demuxer_select_track(track->demuxer, stream, selected); + if (selected) + reinit_subs(mpctx); + return track; + } + } + + struct track *track = talloc_ptrtype(NULL, track); + *track = (struct track) { + .type = stream->type, + .user_tid = find_new_tid(mpctx, stream->type), + .demuxer_id = stream->demuxer_id, + .title = stream->title, + .default_track = stream->default_track, + .attached_picture = stream->attached_picture != NULL, + .lang = stream->lang, + .under_timeline = under_timeline, + .demuxer = stream->demuxer, + .stream = stream, + }; + MP_TARRAY_APPEND(mpctx, mpctx->tracks, mpctx->num_tracks, track); + + if (stream->type == STREAM_SUB) + track->preloaded = !!stream->sub->track; + + // Needed for DVD and Blu-ray. + if (!track->lang) { + struct stream_lang_req req = { + .type = track->type, + .id = map_id_from_demuxer(track->demuxer, track->type, + track->demuxer_id) + }; + stream_control(track->demuxer->stream, STREAM_CTRL_GET_LANG, &req); + if (req.name[0]) + track->lang = talloc_strdup(track, req.name); + } + + demuxer_select_track(track->demuxer, stream, false); + + return track; +} + +static void add_demuxer_tracks(struct MPContext *mpctx, struct demuxer *demuxer) +{ + for (int n = 0; n < demuxer->num_streams; n++) + add_stream_track(mpctx, demuxer->streams[n], !!mpctx->timeline); +} + +static void add_dvd_tracks(struct MPContext *mpctx) +{ +#ifdef CONFIG_DVDREAD + struct demuxer *demuxer = mpctx->demuxer; + struct stream *stream = demuxer->stream; + struct stream_dvd_info_req info; + if (stream_control(stream, STREAM_CTRL_GET_DVD_INFO, &info) > 0) { + for (int n = 0; n < info.num_subs; n++) { + struct track *track = talloc_ptrtype(NULL, track); + *track = (struct track) { + .type = STREAM_SUB, + .user_tid = find_new_tid(mpctx, STREAM_SUB), + .demuxer_id = n, + .demuxer = mpctx->demuxer, + }; + MP_TARRAY_APPEND(mpctx, mpctx->tracks, mpctx->num_tracks, track); + + struct stream_lang_req req = {.type = STREAM_SUB, .id = n}; + stream_control(stream, STREAM_CTRL_GET_LANG, &req); + track->lang = talloc_strdup(track, req.name); + } + } + demuxer_enable_autoselect(demuxer); +#endif +} + +int mp_get_cache_percent(struct MPContext *mpctx) +{ + if (mpctx->stream) { + int64_t size = -1; + int64_t fill = -1; + stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_SIZE, &size); + stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_FILL, &fill); + if (size > 0 && fill >= 0) + return fill / (size / 100); + } + return -1; +} + +static bool mp_get_cache_idle(struct MPContext *mpctx) +{ + int idle = 0; + if (mpctx->stream) + stream_control(mpctx->stream, STREAM_CTRL_GET_CACHE_IDLE, &idle); + return idle; +} + +static void vo_update_window_title(struct MPContext *mpctx) +{ + if (!mpctx->video_out) + return; + char *title = mp_property_expand_string(mpctx, mpctx->opts->wintitle); + if (!mpctx->video_out->window_title || + strcmp(title, mpctx->video_out->window_title)) + { + talloc_free(mpctx->video_out->window_title); + mpctx->video_out->window_title = talloc_steal(mpctx, title); + vo_control(mpctx->video_out, VOCTRL_UPDATE_WINDOW_TITLE, title); + } else { + talloc_free(title); + } +} + +#define saddf(var, ...) (*(var) = talloc_asprintf_append((*var), __VA_ARGS__)) + +// append time in the hh:mm:ss format (plus fractions if wanted) +static void sadd_hhmmssff(char **buf, double time, bool fractions) +{ + char *s = mp_format_time(time, fractions); + *buf = talloc_strdup_append(*buf, s); + talloc_free(s); +} + +static void sadd_percentage(char **buf, int percent) { + if (percent >= 0) + *buf = talloc_asprintf_append(*buf, " (%d%%)", percent); +} + +static int get_term_width(void) +{ + get_screen_size(); + int width = screen_width > 0 ? screen_width : 80; +#if defined(__MINGW32__) || defined(__CYGWIN__) + /* Windows command line is broken (MinGW's rxvt works, but we + * should not depend on that). */ + width--; +#endif + return width; +} + +static void write_status_line(struct MPContext *mpctx, const char *line) +{ + struct MPOpts *opts = mpctx->opts; + if (opts->slave_mode) { + mp_msg(MSGT_STATUSLINE, MSGL_STATUS, "%s\n", line); + } else if (erase_to_end_of_line) { + mp_msg(MSGT_STATUSLINE, MSGL_STATUS, + "%s%s\r", line, erase_to_end_of_line); + } else { + int pos = strlen(line); + int width = get_term_width() - pos; + mp_msg(MSGT_STATUSLINE, MSGL_STATUS, "%s%*s\r", line, width, ""); + } +} + +static void print_status(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + sh_video_t * const sh_video = mpctx->sh_video; + + vo_update_window_title(mpctx); + + if (opts->quiet) + return; + + if (opts->status_msg) { + char *r = mp_property_expand_string(mpctx, opts->status_msg); + write_status_line(mpctx, r); + talloc_free(r); + return; + } + + char *line = NULL; + + // Playback status + if (mpctx->paused_for_cache && !opts->pause) { + saddf(&line, "(Buffering) "); + } else if (mpctx->paused) { + saddf(&line, "(Paused) "); + } + + if (mpctx->sh_audio) + saddf(&line, "A"); + if (mpctx->sh_video) + saddf(&line, "V"); + saddf(&line, ": "); + + // Playback position + double cur = get_current_time(mpctx); + sadd_hhmmssff(&line, cur, mpctx->opts->osd_fractions); + + double len = get_time_length(mpctx); + if (len >= 0) { + saddf(&line, " / "); + sadd_hhmmssff(&line, len, mpctx->opts->osd_fractions); + } + + sadd_percentage(&line, get_percent_pos(mpctx)); + + // other + if (opts->playback_speed != 1) + saddf(&line, " x%4.2f", opts->playback_speed); + + // A-V sync + if (mpctx->sh_audio && sh_video && mpctx->sync_audio_to_video) { + if (mpctx->last_av_difference != MP_NOPTS_VALUE) + saddf(&line, " A-V:%7.3f", mpctx->last_av_difference); + else + saddf(&line, " A-V: ???"); + if (fabs(mpctx->total_avsync_change) > 0.05) + saddf(&line, " ct:%7.3f", mpctx->total_avsync_change); + } + +#ifdef CONFIG_ENCODING + double position = get_current_pos_ratio(mpctx, true); + char lavcbuf[80]; + if (encode_lavc_getstatus(mpctx->encode_lavc_ctx, lavcbuf, sizeof(lavcbuf), + position) >= 0) + { + // encoding stats + saddf(&line, " %s", lavcbuf); + } else +#endif + { + // VO stats + if (sh_video && mpctx->drop_frame_cnt) + saddf(&line, " D: %d", mpctx->drop_frame_cnt); + } + + int cache = mp_get_cache_percent(mpctx); + if (cache >= 0) + saddf(&line, " Cache: %d%%", cache); + + // end + write_status_line(mpctx, line); + talloc_free(line); +} + +typedef struct mp_osd_msg mp_osd_msg_t; +struct mp_osd_msg { + /// Previous message on the stack. + mp_osd_msg_t *prev; + /// Message text. + char *msg; + int id, level, started; + /// Display duration in seconds. + double time; + // Show full OSD for duration of message instead of msg + // (osd_show_progression command) + bool show_position; +}; + +// time is in ms +static mp_osd_msg_t *add_osd_msg(struct MPContext *mpctx, int id, int level, + int time) +{ + rm_osd_msg(mpctx, id); + mp_osd_msg_t *msg = talloc_struct(mpctx, mp_osd_msg_t, { + .prev = mpctx->osd_msg_stack, + .msg = "", + .id = id, + .level = level, + .time = time / 1000.0, + }); + mpctx->osd_msg_stack = msg; + return msg; +} + +static void set_osd_msg_va(struct MPContext *mpctx, int id, int level, int time, + const char *fmt, va_list ap) +{ + if (level == OSD_LEVEL_INVISIBLE) + return; + mp_osd_msg_t *msg = add_osd_msg(mpctx, id, level, time); + msg->msg = talloc_vasprintf(msg, fmt, ap); +} + +void set_osd_msg(struct MPContext *mpctx, int id, int level, int time, + const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + set_osd_msg_va(mpctx, id, level, time, fmt, ap); + va_end(ap); +} + +void set_osd_tmsg(struct MPContext *mpctx, int id, int level, int time, + const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + set_osd_msg_va(mpctx, id, level, time, mp_gtext(fmt), ap); + va_end(ap); +} + +/** + * \brief Remove a message from the OSD stack + * + * This function can be used to get rid of a message right away. + * + */ + +void rm_osd_msg(struct MPContext *mpctx, int id) +{ + mp_osd_msg_t *msg, *last = NULL; + + // Search for the msg + for (msg = mpctx->osd_msg_stack; msg && msg->id != id; + last = msg, msg = msg->prev) ; + if (!msg) + return; + + // Detach it from the stack and free it + if (last) + last->prev = msg->prev; + else + mpctx->osd_msg_stack = msg->prev; + talloc_free(msg); +} + +/** + * \brief Get the current message from the OSD stack. + * + * This function decrements the message timer and destroys the old ones. + * The message that should be displayed is returned (if any). + * + */ + +static mp_osd_msg_t *get_osd_msg(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + mp_osd_msg_t *msg, *prev, *last = NULL; + double now = mp_time_sec(); + double diff; + char hidden_dec_done = 0; + + if (mpctx->osd_visible && now >= mpctx->osd_visible) { + mpctx->osd_visible = 0; + mpctx->osd->progbar_type = -1; // disable + osd_changed(mpctx->osd, OSDTYPE_PROGBAR); + } + if (mpctx->osd_function_visible && now >= mpctx->osd_function_visible) { + mpctx->osd_function_visible = 0; + mpctx->osd_function = 0; + } + + if (!mpctx->osd_last_update) + mpctx->osd_last_update = now; + diff = now >= mpctx->osd_last_update ? now - mpctx->osd_last_update : 0; + + mpctx->osd_last_update = now; + + // Look for the first message in the stack with high enough level. + for (msg = mpctx->osd_msg_stack; msg; last = msg, msg = prev) { + prev = msg->prev; + if (msg->level > opts->osd_level && hidden_dec_done) + continue; + // The message has a high enough level or it is the first hidden one + // in both cases we decrement the timer or kill it. + if (!msg->started || msg->time > diff) { + if (msg->started) + msg->time -= diff; + else + msg->started = 1; + // display it + if (msg->level <= opts->osd_level) + return msg; + hidden_dec_done = 1; + continue; + } + // kill the message + talloc_free(msg); + if (last) { + last->prev = prev; + msg = last; + } else { + mpctx->osd_msg_stack = prev; + msg = NULL; + } + } + // Nothing found + return NULL; +} + +// type: mp_osd_font_codepoints, ASCII, or OSD_BAR_* +// name: fallback for terminal OSD +void set_osd_bar(struct MPContext *mpctx, int type, const char *name, + double min, double max, double val) +{ + struct MPOpts *opts = mpctx->opts; + if (opts->osd_level < 1 || !opts->osd_bar_visible) + return; + + if (mpctx->sh_video && opts->term_osd != 1) { + mpctx->osd_visible = mp_time_sec() + opts->osd_duration / 1000.0; + mpctx->osd->progbar_type = type; + mpctx->osd->progbar_value = (val - min) / (max - min); + mpctx->osd->progbar_num_stops = 0; + osd_changed(mpctx->osd, OSDTYPE_PROGBAR); + return; + } + + set_osd_msg(mpctx, OSD_MSG_BAR, 1, opts->osd_duration, "%s: %d %%", + name, ROUND(100 * (val - min) / (max - min))); +} + +// Update a currently displayed bar of the same type, without resetting the +// timer. +static void update_osd_bar(struct MPContext *mpctx, int type, + double min, double max, double val) +{ + if (mpctx->osd->progbar_type == type) { + float new_value = (val - min) / (max - min); + if (new_value != mpctx->osd->progbar_value) { + mpctx->osd->progbar_value = new_value; + osd_changed(mpctx->osd, OSDTYPE_PROGBAR); + } + } +} + +static void set_osd_bar_chapters(struct MPContext *mpctx, int type) +{ + struct osd_state *osd = mpctx->osd; + osd->progbar_num_stops = 0; + if (osd->progbar_type == type) { + double len = get_time_length(mpctx); + if (len > 0) { + int num = get_chapter_count(mpctx); + for (int n = 0; n < num; n++) { + double time = chapter_start_time(mpctx, n); + if (time >= 0) { + float pos = time / len; + MP_TARRAY_APPEND(osd, osd->progbar_stops, + osd->progbar_num_stops, pos); + } + } + } + } +} + +void set_osd_function(struct MPContext *mpctx, int osd_function) +{ + struct MPOpts *opts = mpctx->opts; + + mpctx->osd_function = osd_function; + mpctx->osd_function_visible = mp_time_sec() + opts->osd_duration / 1000.0; +} + +/** + * \brief Display text subtitles on the OSD + */ +static void set_osd_subtitle(struct MPContext *mpctx, const char *text) +{ + if (!text) + text = ""; + if (strcmp(mpctx->osd->sub_text, text) != 0) { + osd_set_sub(mpctx->osd, text); + if (!mpctx->sh_video) { + rm_osd_msg(mpctx, OSD_MSG_SUB_BASE); + if (text && text[0]) + set_osd_msg(mpctx, OSD_MSG_SUB_BASE, 1, INT_MAX, "%s", text); + } + } + if (!text[0]) + rm_osd_msg(mpctx, OSD_MSG_SUB_BASE); +} + +// sym == mpctx->osd_function +static void saddf_osd_function_sym(char **buffer, int sym) +{ + char temp[10]; + osd_get_function_sym(temp, sizeof(temp), sym); + saddf(buffer, "%s ", temp); +} + +static void sadd_osd_status(char **buffer, struct MPContext *mpctx, bool full) +{ + bool fractions = mpctx->opts->osd_fractions; + int sym = mpctx->osd_function; + if (!sym) { + if (mpctx->paused_for_cache && !mpctx->opts->pause) { + sym = OSD_CLOCK; + } else if (mpctx->paused || mpctx->step_frames) { + sym = OSD_PAUSE; + } else { + sym = OSD_PLAY; + } + } + saddf_osd_function_sym(buffer, sym); + char *custom_msg = mpctx->opts->osd_status_msg; + if (custom_msg && full) { + char *text = mp_property_expand_string(mpctx, custom_msg); + *buffer = talloc_strdup_append(*buffer, text); + talloc_free(text); + } else { + sadd_hhmmssff(buffer, get_current_time(mpctx), fractions); + if (full) { + saddf(buffer, " / "); + sadd_hhmmssff(buffer, get_time_length(mpctx), fractions); + sadd_percentage(buffer, get_percent_pos(mpctx)); + int cache = mp_get_cache_percent(mpctx); + if (cache >= 0) + saddf(buffer, " Cache: %d%%", cache); + } + } +} + +// OSD messages initated by seeking commands are added lazily with this +// function, because multiple successive seek commands can be coalesced. +static void add_seek_osd_messages(struct MPContext *mpctx) +{ + if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_BAR) { + set_osd_bar(mpctx, OSD_BAR_SEEK, "Position", 0, 1, + av_clipf(get_current_pos_ratio(mpctx, false), 0, 1)); + set_osd_bar_chapters(mpctx, OSD_BAR_SEEK); + } + if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_TEXT) { + mp_osd_msg_t *msg = add_osd_msg(mpctx, OSD_MSG_TEXT, 1, + mpctx->opts->osd_duration); + msg->show_position = true; + } + if (mpctx->add_osd_seek_info & OSD_SEEK_INFO_CHAPTER_TEXT) { + char *chapter = chapter_display_name(mpctx, get_current_chapter(mpctx)); + set_osd_tmsg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration, + "Chapter: %s", chapter); + talloc_free(chapter); + } + if ((mpctx->add_osd_seek_info & OSD_SEEK_INFO_EDITION) + && mpctx->master_demuxer) + { + set_osd_tmsg(mpctx, OSD_MSG_TEXT, 1, mpctx->opts->osd_duration, + "Playing edition %d of %d.", + mpctx->master_demuxer->edition + 1, + mpctx->master_demuxer->num_editions); + } + mpctx->add_osd_seek_info = 0; +} + +/** + * \brief Update the OSD message line. + * + * This function displays the current message on the vo OSD or on the term. + * If the stack is empty and the OSD level is high enough the timer + * is displayed (only on the vo OSD). + * + */ + +static void update_osd_msg(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + struct osd_state *osd = mpctx->osd; + + add_seek_osd_messages(mpctx); + update_osd_bar(mpctx, OSD_BAR_SEEK, 0, 1, + av_clipf(get_current_pos_ratio(mpctx, false), 0, 1)); + + // Look if we have a msg + mp_osd_msg_t *msg = get_osd_msg(mpctx); + if (msg && !msg->show_position) { + if (mpctx->sh_video && opts->term_osd != 1) { + osd_set_text(osd, msg->msg); + } else if (opts->term_osd) { + if (strcmp(mpctx->terminal_osd_text, msg->msg)) { + talloc_free(mpctx->terminal_osd_text); + mpctx->terminal_osd_text = talloc_strdup(mpctx, msg->msg); + // Multi-line message => clear what will be the second line + write_status_line(mpctx, ""); + mp_msg(MSGT_CPLAYER, MSGL_STATUS, "%s%s\n", opts->term_osd_esc, + mpctx->terminal_osd_text); + print_status(mpctx); + } + } + return; + } + + int osd_level = opts->osd_level; + if (msg && msg->show_position) + osd_level = 3; + + if (mpctx->sh_video && opts->term_osd != 1) { + // fallback on the timer + char *text = NULL; + + if (osd_level >= 2) + sadd_osd_status(&text, mpctx, osd_level == 3); + + osd_set_text(osd, text); + talloc_free(text); + return; + } + + // Clear the term osd line + if (opts->term_osd && mpctx->terminal_osd_text[0]) { + mpctx->terminal_osd_text[0] = '\0'; + mp_msg(MSGT_CPLAYER, MSGL_STATUS, "%s\n", opts->term_osd_esc); + } +} + +static int build_afilter_chain(struct MPContext *mpctx) +{ + struct sh_audio *sh_audio = mpctx->sh_audio; + struct ao *ao = mpctx->ao; + struct MPOpts *opts = mpctx->opts; + int new_srate; + if (af_control_any_rev(sh_audio->afilter, + AF_CONTROL_PLAYBACK_SPEED | AF_CONTROL_SET, + &opts->playback_speed)) + new_srate = sh_audio->samplerate; + else { + new_srate = sh_audio->samplerate * opts->playback_speed; + if (new_srate != ao->samplerate) { + // limits are taken from libaf/af_resample.c + if (new_srate < 8000) + new_srate = 8000; + if (new_srate > 192000) + new_srate = 192000; + opts->playback_speed = (double)new_srate / sh_audio->samplerate; + } + } + return init_audio_filters(sh_audio, new_srate, + &ao->samplerate, &ao->channels, &ao->format); +} + +static int recreate_audio_filters(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + assert(mpctx->sh_audio); + + // init audio filters: + if (!build_afilter_chain(mpctx)) { + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, + "Couldn't find matching filter/ao format!\n"); + return -1; + } + + mpctx->mixer.afilter = mpctx->sh_audio->afilter; + mpctx->mixer.volstep = opts->volstep; + mpctx->mixer.softvol = opts->softvol; + mpctx->mixer.softvol_max = opts->softvol_max; + mixer_reinit(&mpctx->mixer, mpctx->ao); + if (!(mpctx->initialized_flags & INITIALIZED_VOL)) { + if (opts->mixer_init_volume >= 0) { + mixer_setvolume(&mpctx->mixer, opts->mixer_init_volume, + opts->mixer_init_volume); + } + if (opts->mixer_init_mute >= 0) + mixer_setmute(&mpctx->mixer, opts->mixer_init_mute); + mpctx->initialized_flags |= INITIALIZED_VOL; + } + + return 0; +} + +int reinit_audio_filters(struct MPContext *mpctx) +{ + struct sh_audio *sh_audio = mpctx->sh_audio; + if (!sh_audio) + return -2; + + af_uninit(mpctx->sh_audio->afilter); + if (af_init(mpctx->sh_audio->afilter) < 0) + return -1; + if (recreate_audio_filters(mpctx) < 0) + return -1; + + return 0; +} + +void reinit_audio_chain(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + init_demux_stream(mpctx, STREAM_AUDIO); + if (!mpctx->sh_audio) { + uninit_player(mpctx, INITIALIZED_VOL | INITIALIZED_AO); + goto no_audio; + } + + if (!(mpctx->initialized_flags & INITIALIZED_ACODEC)) { + if (!init_best_audio_codec(mpctx->sh_audio, opts->audio_decoders)) + goto init_error; + mpctx->initialized_flags |= INITIALIZED_ACODEC; + } + + int ao_srate = opts->force_srate; + int ao_format = opts->audio_output_format; + struct mp_chmap ao_channels = {0}; + if (mpctx->initialized_flags & INITIALIZED_AO) { + ao_srate = mpctx->ao->samplerate; + ao_format = mpctx->ao->format; + ao_channels = mpctx->ao->channels; + } else { + // Automatic downmix + if (mp_chmap_is_stereo(&opts->audio_output_channels) && + !mp_chmap_is_stereo(&mpctx->sh_audio->channels)) + { + mp_chmap_from_channels(&ao_channels, 2); + } + } + + // Determine what the filter chain outputs. build_afilter_chain() also + // needs this for testing whether playback speed is changed by resampling + // or using a special filter. + if (!init_audio_filters(mpctx->sh_audio, // preliminary init + // input: + mpctx->sh_audio->samplerate, + // output: + &ao_srate, &ao_channels, &ao_format)) { + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, "Error at audio filter chain " + "pre-init!\n"); + goto init_error; + } + + if (!(mpctx->initialized_flags & INITIALIZED_AO)) { + mpctx->initialized_flags |= INITIALIZED_AO; + mp_chmap_remove_useless_channels(&ao_channels, + &opts->audio_output_channels); + mpctx->ao = ao_init_best(mpctx->global, mpctx->input, + mpctx->encode_lavc_ctx, ao_srate, ao_format, + ao_channels); + struct ao *ao = mpctx->ao; + if (!ao) { + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, + "Could not open/initialize audio device -> no sound.\n"); + goto init_error; + } + ao->buffer.start = talloc_new(ao); + char *s = mp_audio_fmt_to_str(ao->samplerate, &ao->channels, ao->format); + mp_msg(MSGT_CPLAYER, MSGL_INFO, "AO: [%s] %s\n", + ao->driver->info->short_name, s); + talloc_free(s); + mp_msg(MSGT_CPLAYER, MSGL_V, "AO: Description: %s\nAO: Author: %s\n", + ao->driver->info->name, ao->driver->info->author); + if (strlen(ao->driver->info->comment) > 0) + mp_msg(MSGT_CPLAYER, MSGL_V, "AO: Comment: %s\n", + ao->driver->info->comment); + } + + if (recreate_audio_filters(mpctx) < 0) + goto init_error; + + mpctx->syncing_audio = true; + return; + +init_error: + uninit_player(mpctx, INITIALIZED_ACODEC | INITIALIZED_AO | INITIALIZED_VOL); + cleanup_demux_stream(mpctx, STREAM_AUDIO); +no_audio: + mpctx->current_track[STREAM_AUDIO] = NULL; + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, "Audio: no audio\n"); +} + + +// Return pts value corresponding to the end point of audio written to the +// ao so far. +static double written_audio_pts(struct MPContext *mpctx) +{ + sh_audio_t *sh_audio = mpctx->sh_audio; + if (!sh_audio) + return MP_NOPTS_VALUE; + + double bps = sh_audio->channels.num * sh_audio->samplerate * + sh_audio->samplesize; + + // first calculate the end pts of audio that has been output by decoder + double a_pts = sh_audio->pts; + if (a_pts == MP_NOPTS_VALUE) + return MP_NOPTS_VALUE; + + // sh_audio->pts is the timestamp of the latest input packet with + // known pts that the decoder has decoded. sh_audio->pts_bytes is + // the amount of bytes the decoder has written after that timestamp. + a_pts += sh_audio->pts_bytes / bps; + + // Now a_pts hopefully holds the pts for end of audio from decoder. + // Subtract data in buffers between decoder and audio out. + + // Decoded but not filtered + a_pts -= sh_audio->a_buffer_len / bps; + + // Data buffered in audio filters, measured in bytes of "missing" output + double buffered_output = af_calc_delay(sh_audio->afilter); + + // Data that was ready for ao but was buffered because ao didn't fully + // accept everything to internal buffers yet + buffered_output += mpctx->ao->buffer.len; + + // Filters divide audio length by playback_speed, so multiply by it + // to get the length in original units without speedup or slowdown + a_pts -= buffered_output * mpctx->opts->playback_speed / mpctx->ao->bps; + + return a_pts + mpctx->video_offset; +} + +// Return pts value corresponding to currently playing audio. +double playing_audio_pts(struct MPContext *mpctx) +{ + double pts = written_audio_pts(mpctx); + if (pts == MP_NOPTS_VALUE) + return pts; + return pts - mpctx->opts->playback_speed * ao_get_delay(mpctx->ao); +} + +// When reading subtitles from a demuxer, and we read video or audio from the +// demuxer, we should not explicitly read subtitle packets. (With external +// subs, we have to.) +static bool is_interleaved(struct MPContext *mpctx, struct track *track) +{ + if (track->is_external || !track->demuxer) + return false; + + struct demuxer *demuxer = track->demuxer; + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + struct track *other = mpctx->current_track[type]; + if (other && other != track && other->demuxer && other->demuxer == demuxer) + return true; + } + return false; +} + +static void reset_subtitles(struct MPContext *mpctx) +{ + if (mpctx->sh_sub) + sub_reset(mpctx->sh_sub->dec_sub); + set_osd_subtitle(mpctx, NULL); + osd_changed(mpctx->osd, OSDTYPE_SUB); +} + +static void update_subtitles(struct MPContext *mpctx, double refpts_tl) +{ + struct MPOpts *opts = mpctx->opts; + if (!(mpctx->initialized_flags & INITIALIZED_SUB)) + return; + + struct track *track = mpctx->current_track[STREAM_SUB]; + struct sh_sub *sh_sub = mpctx->sh_sub; + assert(track && sh_sub); + struct dec_sub *dec_sub = sh_sub->dec_sub; + + if (mpctx->sh_video) { + struct mp_image_params params; + if (get_video_params(mpctx->sh_video, ¶ms) > 0) + sub_control(dec_sub, SD_CTRL_SET_VIDEO_PARAMS, ¶ms); + } + + mpctx->osd->video_offset = track->under_timeline ? mpctx->video_offset : 0; + + double refpts_s = refpts_tl - mpctx->osd->video_offset; + double curpts_s = refpts_s + opts->sub_delay; + + if (!track->preloaded) { + bool interleaved = is_interleaved(mpctx, track); + + while (1) { + if (interleaved && !demux_has_packet(sh_sub->gsh)) + break; + double subpts_s = demux_get_next_pts(sh_sub->gsh); + if (!demux_has_packet(sh_sub->gsh)) + break; + if (subpts_s > curpts_s) { + mp_dbg(MSGT_CPLAYER, MSGL_DBG2, + "Sub early: c_pts=%5.3f s_pts=%5.3f\n", + curpts_s, subpts_s); + // Libass handled subs can be fed to it in advance + if (!sub_accept_packets_in_advance(dec_sub)) + break; + // Try to avoid demuxing whole file at once + if (subpts_s > curpts_s + 1 && !interleaved) + break; + } + struct demux_packet *pkt = demux_read_packet(sh_sub->gsh); + mp_dbg(MSGT_CPLAYER, MSGL_V, "Sub: c_pts=%5.3f s_pts=%5.3f " + "duration=%5.3f len=%d\n", curpts_s, pkt->pts, pkt->duration, + pkt->len); + sub_decode(dec_sub, pkt); + talloc_free(pkt); + } + } + + if (!mpctx->osd->render_bitmap_subs || !mpctx->sh_video) + set_osd_subtitle(mpctx, sub_get_text(dec_sub, curpts_s)); +} + +static int check_framedrop(struct MPContext *mpctx, double frame_time) +{ + struct MPOpts *opts = mpctx->opts; + // check for frame-drop: + if (mpctx->sh_audio && !mpctx->ao->untimed && + !demux_stream_eof(mpctx->sh_audio->gsh)) + { + float delay = opts->playback_speed * ao_get_delay(mpctx->ao); + float d = delay - mpctx->delay; + if (frame_time < 0) + frame_time = mpctx->sh_video->fps > 0 ? 1.0 / mpctx->sh_video->fps : 0; + // we should avoid dropping too many frames in sequence unless we + // are too late. and we allow 100ms A-V delay here: + if (d < -mpctx->dropped_frames * frame_time - 0.100 && !mpctx->paused + && !mpctx->restart_playback) { + mpctx->drop_frame_cnt++; + mpctx->dropped_frames++; + return mpctx->opts->frame_dropping; + } else + mpctx->dropped_frames = 0; + } + return 0; +} + +static double timing_sleep(struct MPContext *mpctx, double time_frame) +{ + // assume kernel HZ=100 for softsleep, works with larger HZ but with + // unnecessarily high CPU usage + struct MPOpts *opts = mpctx->opts; + double margin = opts->softsleep ? 0.011 : 0; + while (time_frame > margin) { + mp_sleep_us(1000000 * (time_frame - margin)); + time_frame -= get_relative_time(mpctx); + } + if (opts->softsleep) { + if (time_frame < 0) + mp_tmsg(MSGT_AVSYNC, MSGL_WARN, + "Warning! Softsleep underflow!\n"); + while (time_frame > 0) + time_frame -= get_relative_time(mpctx); // burn the CPU + } + return time_frame; +} + +static void set_dvdsub_fake_extradata(struct dec_sub *dec_sub, struct stream *st, + int width, int height) +{ +#ifdef CONFIG_DVDREAD + if (!st) + return; + + struct stream_dvd_info_req info; + if (stream_control(st, STREAM_CTRL_GET_DVD_INFO, &info) < 0) + return; + + struct mp_csp_params csp = MP_CSP_PARAMS_DEFAULTS; + csp.int_bits_in = 8; + csp.int_bits_out = 8; + float cmatrix[3][4]; + mp_get_yuv2rgb_coeffs(&csp, cmatrix); + + if (width == 0 || height == 0) { + width = 720; + height = 480; + } + + char *s = NULL; + s = talloc_asprintf_append(s, "size: %dx%d\n", width, height); + s = talloc_asprintf_append(s, "palette: "); + for (int i = 0; i < 16; i++) { + int color = info.palette[i]; + int c[3] = {(color >> 16) & 0xff, (color >> 8) & 0xff, color & 0xff}; + mp_map_int_color(cmatrix, 8, c); + color = (c[2] << 16) | (c[1] << 8) | c[0]; + + if (i != 0) + talloc_asprintf_append(s, ", "); + s = talloc_asprintf_append(s, "%06x", color); + } + s = talloc_asprintf_append(s, "\n"); + + sub_set_extradata(dec_sub, s, strlen(s)); + talloc_free(s); +#endif +} + +static void reinit_subs(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + struct track *track = mpctx->current_track[STREAM_SUB]; + + assert(!(mpctx->initialized_flags & INITIALIZED_SUB)); + + init_demux_stream(mpctx, STREAM_SUB); + if (!mpctx->sh_sub) + return; + + if (!mpctx->sh_sub->dec_sub) + mpctx->sh_sub->dec_sub = sub_create(opts); + + assert(track->demuxer); + // Lazily added DVD track - will be created on first sub packet + if (!track->stream) + return; + + mpctx->initialized_flags |= INITIALIZED_SUB; + + struct sh_sub *sh_sub = mpctx->sh_sub; + struct dec_sub *dec_sub = sh_sub->dec_sub; + assert(dec_sub); + + if (!sub_is_initialized(dec_sub)) { + int w = mpctx->sh_video ? mpctx->sh_video->disp_w : 0; + int h = mpctx->sh_video ? mpctx->sh_video->disp_h : 0; + float fps = mpctx->sh_video ? mpctx->sh_video->fps : 25; + + set_dvdsub_fake_extradata(dec_sub, track->demuxer->stream, w, h); + sub_set_video_res(dec_sub, w, h); + sub_set_video_fps(dec_sub, fps); + sub_set_ass_renderer(dec_sub, mpctx->osd->ass_library, + mpctx->osd->ass_renderer); + sub_init_from_sh(dec_sub, sh_sub); + + // Don't do this if the file has video/audio streams. Don't do it even + // if it has only sub streams, because reading packets will change the + // demuxer position. + if (!track->preloaded && track->is_external) { + demux_seek(track->demuxer, 0, 0, SEEK_ABSOLUTE); + track->preloaded = sub_read_all_packets(dec_sub, sh_sub); + } + } + + mpctx->osd->dec_sub = dec_sub; + + // Decides whether to use OSD path or normal subtitle rendering path. + mpctx->osd->render_bitmap_subs = + opts->ass_enabled || !sub_has_get_text(dec_sub); + + reset_subtitles(mpctx); +} + +static char *track_layout_hash(struct MPContext *mpctx) +{ + char *h = talloc_strdup(NULL, ""); + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *track = mpctx->tracks[n]; + if (track->type != type) + continue; + h = talloc_asprintf_append_buffer(h, "%d-%d-%d-%d-%s\n", type, + track->user_tid, track->default_track, track->is_external, + track->lang ? track->lang : ""); + } + } + return h; +} + +void mp_switch_track(struct MPContext *mpctx, enum stream_type type, + struct track *track) +{ + assert(!track || track->type == type); + + struct track *current = mpctx->current_track[type]; + if (track == current) + return; + + if (type == STREAM_VIDEO) { + uninit_player(mpctx, INITIALIZED_VCODEC | + (mpctx->opts->fixed_vo && track ? 0 : INITIALIZED_VO)); + } else if (type == STREAM_AUDIO) { + uninit_player(mpctx, INITIALIZED_AO | INITIALIZED_ACODEC | INITIALIZED_VOL); + } else if (type == STREAM_SUB) { + uninit_player(mpctx, INITIALIZED_SUB); + } + + mpctx->current_track[type] = track; + + int user_tid = track ? track->user_tid : -2; + if (type == STREAM_VIDEO) { + mpctx->opts->video_id = user_tid; + reinit_video_chain(mpctx); + } else if (type == STREAM_AUDIO) { + mpctx->opts->audio_id = user_tid; + reinit_audio_chain(mpctx); + } else if (type == STREAM_SUB) { + mpctx->opts->sub_id = user_tid; + reinit_subs(mpctx); + } + + talloc_free(mpctx->track_layout_hash); + mpctx->track_layout_hash = talloc_steal(mpctx, track_layout_hash(mpctx)); +} + +struct track *mp_track_by_tid(struct MPContext *mpctx, enum stream_type type, + int tid) +{ + if (tid == -1) + return mpctx->current_track[type]; + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *track = mpctx->tracks[n]; + if (track->type == type && track->user_tid == tid) + return track; + } + return NULL; +} + +bool mp_remove_track(struct MPContext *mpctx, struct track *track) +{ + if (track->under_timeline) + return false; + if (!track->is_external) + return false; + + if (mpctx->current_track[track->type] == track) { + mp_switch_track(mpctx, track->type, NULL); + if (mpctx->current_track[track->type] == track) + return false; + } + + int index = 0; + while (index < mpctx->num_tracks && mpctx->tracks[index] != track) + index++; + assert(index < mpctx->num_tracks); + while (index + 1 < mpctx->num_tracks) { + mpctx->tracks[index] = mpctx->tracks[index + 1]; + index++; + } + mpctx->num_tracks--; + talloc_free(track); + return true; +} + +/* Modify video timing to match the audio timeline. There are two main + * reasons this is needed. First, video and audio can start from different + * positions at beginning of file or after a seek (MPlayer starts both + * immediately even if they have different pts). Second, the file can have + * audio timestamps that are inconsistent with the duration of the audio + * packets, for example two consecutive timestamp values differing by + * one second but only a packet with enough samples for half a second + * of playback between them. + */ +static void adjust_sync(struct MPContext *mpctx, double frame_time) +{ + struct MPOpts *opts = mpctx->opts; + + if (!mpctx->sh_audio || mpctx->syncing_audio) + return; + + double a_pts = written_audio_pts(mpctx) - mpctx->delay; + double v_pts = mpctx->sh_video->pts; + double av_delay = a_pts - v_pts; + // Try to sync vo_flip() so it will *finish* at given time + av_delay += mpctx->last_vo_flip_duration; + av_delay -= mpctx->audio_delay; // This much pts difference is desired + + double change = av_delay * 0.1; + double max_change = opts->default_max_pts_correction >= 0 ? + opts->default_max_pts_correction : frame_time * 0.1; + if (change < -max_change) + change = -max_change; + else if (change > max_change) + change = max_change; + mpctx->delay += change; + mpctx->total_avsync_change += change; +} + +static int write_to_ao(struct MPContext *mpctx, void *data, int len, int flags, + double pts) +{ + if (mpctx->paused) + return 0; + struct ao *ao = mpctx->ao; + double bps = ao->bps / mpctx->opts->playback_speed; + ao->pts = pts; + int played = ao_play(mpctx->ao, data, len, flags); + if (played > 0) { + mpctx->delay += played / bps; + // Keep correct pts for remaining data - could be used to flush + // remaining buffer when closing ao. + ao->pts += played / bps; + return played; + } + return 0; +} + +#define ASYNC_PLAY_DONE -3 +static int audio_start_sync(struct MPContext *mpctx, int playsize) +{ + struct ao *ao = mpctx->ao; + struct MPOpts *opts = mpctx->opts; + sh_audio_t * const sh_audio = mpctx->sh_audio; + int res; + + // Timing info may not be set without + res = decode_audio(sh_audio, &ao->buffer, 1); + if (res < 0) + return res; + + int bytes; + bool did_retry = false; + double written_pts; + double bps = ao->bps / opts->playback_speed; + bool hrseek = mpctx->hrseek_active; // audio only hrseek + mpctx->hrseek_active = false; + while (1) { + written_pts = written_audio_pts(mpctx); + double ptsdiff; + if (hrseek) + ptsdiff = written_pts - mpctx->hrseek_pts; + else + ptsdiff = written_pts - mpctx->sh_video->pts - mpctx->delay + - mpctx->audio_delay; + bytes = ptsdiff * bps; + bytes -= bytes % (ao->channels.num * af_fmt2bits(ao->format) / 8); + + // ogg demuxers give packets without timing + if (written_pts <= 1 && sh_audio->pts == MP_NOPTS_VALUE) { + if (!did_retry) { + // Try to read more data to see packets that have pts + res = decode_audio(sh_audio, &ao->buffer, ao->bps); + if (res < 0) + return res; + did_retry = true; + continue; + } + bytes = 0; + } + + if (fabs(ptsdiff) > 300 || isnan(ptsdiff)) // pts reset or just broken? + bytes = 0; + + if (bytes > 0) + break; + + mpctx->syncing_audio = false; + int a = FFMIN(-bytes, FFMAX(playsize, 20000)); + res = decode_audio(sh_audio, &ao->buffer, a); + bytes += ao->buffer.len; + if (bytes >= 0) { + memmove(ao->buffer.start, + ao->buffer.start + ao->buffer.len - bytes, bytes); + ao->buffer.len = bytes; + if (res < 0) + return res; + return decode_audio(sh_audio, &ao->buffer, playsize); + } + ao->buffer.len = 0; + if (res < 0) + return res; + } + if (hrseek) + // Don't add silence in audio-only case even if position is too late + return 0; + int fillbyte = 0; + if ((ao->format & AF_FORMAT_SIGN_MASK) == AF_FORMAT_US) + fillbyte = 0x80; + if (bytes >= playsize) { + /* This case could fall back to the one below with + * bytes = playsize, but then silence would keep accumulating + * in a_out_buffer if the AO accepts less data than it asks for + * in playsize. */ + char *p = malloc(playsize); + memset(p, fillbyte, playsize); + write_to_ao(mpctx, p, playsize, 0, written_pts - bytes / bps); + free(p); + return ASYNC_PLAY_DONE; + } + mpctx->syncing_audio = false; + decode_audio_prepend_bytes(&ao->buffer, bytes, fillbyte); + return decode_audio(sh_audio, &ao->buffer, playsize); +} + +static int fill_audio_out_buffers(struct MPContext *mpctx, double endpts) +{ + struct MPOpts *opts = mpctx->opts; + struct ao *ao = mpctx->ao; + int playsize; + int playflags = 0; + bool audio_eof = false; + bool partial_fill = false; + sh_audio_t * const sh_audio = mpctx->sh_audio; + bool modifiable_audio_format = !(ao->format & AF_FORMAT_SPECIAL_MASK); + int unitsize = ao->channels.num * af_fmt2bits(ao->format) / 8; + + if (mpctx->paused) + playsize = 1; // just initialize things (audio pts at least) + else + playsize = ao_get_space(ao); + + // Coming here with hrseek_active still set means audio-only + if (!mpctx->sh_video || !mpctx->sync_audio_to_video) + mpctx->syncing_audio = false; + if (!opts->initial_audio_sync || !modifiable_audio_format) { + mpctx->syncing_audio = false; + mpctx->hrseek_active = false; + } + + int res; + if (mpctx->syncing_audio || mpctx->hrseek_active) + res = audio_start_sync(mpctx, playsize); + else + res = decode_audio(sh_audio, &ao->buffer, playsize); + + if (res < 0) { // EOF, error or format change + if (res == -2) { + /* The format change isn't handled too gracefully. A more precise + * implementation would require draining buffered old-format audio + * while displaying video, then doing the output format switch. + */ + if (!mpctx->opts->gapless_audio) + uninit_player(mpctx, INITIALIZED_AO | INITIALIZED_VOL); + reinit_audio_chain(mpctx); + return -1; + } else if (res == ASYNC_PLAY_DONE) + return 0; + else if (demux_stream_eof(mpctx->sh_audio->gsh)) + audio_eof = true; + } + + if (endpts != MP_NOPTS_VALUE && modifiable_audio_format) { + double bytes = (endpts - written_audio_pts(mpctx) + mpctx->audio_delay) + * ao->bps / opts->playback_speed; + if (playsize > bytes) { + playsize = FFMAX(bytes, 0); + playflags |= AOPLAY_FINAL_CHUNK; + audio_eof = true; + partial_fill = true; + } + } + + assert(ao->buffer.len % unitsize == 0); + if (playsize > ao->buffer.len) { + partial_fill = true; + playsize = ao->buffer.len; + if (audio_eof) + playflags |= AOPLAY_FINAL_CHUNK; + } + playsize -= playsize % unitsize; + if (!playsize) + return partial_fill && audio_eof ? -2 : -partial_fill; + + // play audio: + + int played = write_to_ao(mpctx, ao->buffer.start, playsize, playflags, + written_audio_pts(mpctx)); + assert(played % unitsize == 0); + ao->buffer_playable_size = playsize - played; + + if (played > 0) { + ao->buffer.len -= played; + memmove(ao->buffer.start, ao->buffer.start + played, ao->buffer.len); + } else if (!mpctx->paused && audio_eof && ao_get_delay(ao) < .04) { + // Sanity check to avoid hanging in case current ao doesn't output + // partial chunks and doesn't check for AOPLAY_FINAL_CHUNK + return -2; + } + + return -partial_fill; +} + +static void update_fps(struct MPContext *mpctx) +{ +#ifdef CONFIG_ENCODING + struct sh_video *sh_video = mpctx->sh_video; + if (mpctx->encode_lavc_ctx && sh_video) + encode_lavc_set_video_fps(mpctx->encode_lavc_ctx, sh_video->fps); +#endif +} + +static void recreate_video_filters(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + struct sh_video *sh_video = mpctx->sh_video; + assert(sh_video); + + vf_uninit_filter_chain(sh_video->vfilter); + + char *vf_arg[] = { + "_oldargs_", (char *)mpctx->video_out, NULL + }; + sh_video->vfilter = vf_open_filter(opts, NULL, "vo", vf_arg); + + sh_video->vfilter = append_filters(sh_video->vfilter, opts->vf_settings); + + struct vf_instance *vf = sh_video->vfilter; + mpctx->osd->render_subs_in_filter + = vf->control(vf, VFCTRL_INIT_OSD, NULL) == VO_TRUE; +} + +int reinit_video_filters(struct MPContext *mpctx) +{ + struct sh_video *sh_video = mpctx->sh_video; + + if (!sh_video) + return -2; + + recreate_video_filters(mpctx); + video_reinit_vo(sh_video); + + return sh_video->vf_initialized > 0 ? 0 : -1; +} + +int reinit_video_chain(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + assert(!(mpctx->initialized_flags & INITIALIZED_VCODEC)); + init_demux_stream(mpctx, STREAM_VIDEO); + sh_video_t *sh_video = mpctx->sh_video; + if (!sh_video) { + uninit_player(mpctx, INITIALIZED_VO); + goto no_video; + } + + mp_tmsg(MSGT_CPLAYER, MSGL_V, "[V] fourcc:0x%X " + "size:%dx%d fps:%5.3f\n", + mpctx->sh_video->format, + mpctx->sh_video->disp_w, mpctx->sh_video->disp_h, + mpctx->sh_video->fps); + if (opts->force_fps) + mpctx->sh_video->fps = opts->force_fps; + update_fps(mpctx); + + if (!mpctx->sh_video->fps && !opts->force_fps && !opts->correct_pts) { + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, "FPS not specified in the " + "header or invalid, use the -fps option.\n"); + } + + double ar = -1.0; + //================== Init VIDEO (codec & libvo) ========================== + if (!opts->fixed_vo || !(mpctx->initialized_flags & INITIALIZED_VO)) { + mpctx->video_out = init_best_video_out(mpctx->global, mpctx->input, + mpctx->encode_lavc_ctx); + if (!mpctx->video_out) { + mp_tmsg(MSGT_CPLAYER, MSGL_FATAL, "Error opening/initializing " + "the selected video_out (-vo) device.\n"); + goto err_out; + } + if (opts->vo.cursor_autohide_delay != -1) { + vo_control(mpctx->video_out, VOCTRL_SET_CURSOR_VISIBILITY, + &(bool){false}); + } + mpctx->initialized_flags |= INITIALIZED_VO; + + // dynamic allocation only to make stheader.h lighter + talloc_free(sh_video->hwdec_info); + sh_video->hwdec_info = talloc_zero(sh_video, struct mp_hwdec_info); + vo_control(mpctx->video_out, VOCTRL_GET_HWDEC_INFO, sh_video->hwdec_info); + } + + vo_update_window_title(mpctx); + + if (stream_control(mpctx->sh_video->gsh->demuxer->stream, + STREAM_CTRL_GET_ASPECT_RATIO, &ar) != STREAM_UNSUPPORTED) + mpctx->sh_video->stream_aspect = ar; + + recreate_video_filters(mpctx); + + init_best_video_codec(sh_video, opts->video_decoders); + + if (!sh_video->initialized) + goto err_out; + + mpctx->initialized_flags |= INITIALIZED_VCODEC; + + bool saver_state = opts->pause || !opts->stop_screensaver; + vo_control(mpctx->video_out, saver_state ? VOCTRL_RESTORE_SCREENSAVER + : VOCTRL_KILL_SCREENSAVER, NULL); + + vo_control(mpctx->video_out, mpctx->paused ? VOCTRL_PAUSE + : VOCTRL_RESUME, NULL); + + sh_video->last_pts = MP_NOPTS_VALUE; + sh_video->num_buffered_pts = 0; + sh_video->next_frame_time = 0; + mpctx->restart_playback = true; + mpctx->sync_audio_to_video = !sh_video->gsh->attached_picture; + mpctx->delay = 0; + mpctx->vo_pts_history_seek_ts++; + + reset_subtitles(mpctx); + + return 1; + +err_out: + uninit_player(mpctx, INITIALIZED_VO); + cleanup_demux_stream(mpctx, STREAM_VIDEO); +no_video: + mpctx->current_track[STREAM_VIDEO] = NULL; + mpctx->sync_audio_to_video = false; + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, "Video: no video\n"); + return 0; +} + +// Try to refresh the video by doing a precise seek to the currently displayed +// frame. This can go wrong in all sorts of ways, so use sparingly. +void mp_force_video_refresh(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + + // If not paused, the next frame should come soon enough. + if (opts->pause && mpctx->last_vo_pts != MP_NOPTS_VALUE) + queue_seek(mpctx, MPSEEK_ABSOLUTE, mpctx->last_vo_pts, 1); +} + +static void add_frame_pts(struct MPContext *mpctx, double pts) +{ + if (pts == MP_NOPTS_VALUE || mpctx->hrseek_framedrop) { + mpctx->vo_pts_history_seek_ts++; // mark discontinuity + return; + } + for (int n = MAX_NUM_VO_PTS - 1; n >= 1; n--) { + mpctx->vo_pts_history_seek[n] = mpctx->vo_pts_history_seek[n - 1]; + mpctx->vo_pts_history_pts[n] = mpctx->vo_pts_history_pts[n - 1]; + } + mpctx->vo_pts_history_seek[0] = mpctx->vo_pts_history_seek_ts; + mpctx->vo_pts_history_pts[0] = pts; +} + +static double find_previous_pts(struct MPContext *mpctx, double pts) +{ + for (int n = 0; n < MAX_NUM_VO_PTS - 1; n++) { + if (pts == mpctx->vo_pts_history_pts[n] && + mpctx->vo_pts_history_seek[n] != 0 && + mpctx->vo_pts_history_seek[n] == mpctx->vo_pts_history_seek[n + 1]) + { + return mpctx->vo_pts_history_pts[n + 1]; + } + } + return MP_NOPTS_VALUE; +} + +static double get_last_frame_pts(struct MPContext *mpctx) +{ + if (mpctx->vo_pts_history_seek[0] == mpctx->vo_pts_history_seek_ts) + return mpctx->vo_pts_history_pts[0]; + return MP_NOPTS_VALUE; +} + +static bool filter_output_queued_frame(struct MPContext *mpctx) +{ + struct sh_video *sh_video = mpctx->sh_video; + struct vo *video_out = mpctx->video_out; + + struct mp_image *img = vf_chain_output_queued_frame(sh_video->vfilter); + if (img) + vo_queue_image(video_out, img); + talloc_free(img); + + return !!img; +} + +static bool load_next_vo_frame(struct MPContext *mpctx, bool eof) +{ + if (vo_get_buffered_frame(mpctx->video_out, eof) >= 0) + return true; + if (filter_output_queued_frame(mpctx)) + return true; + return false; +} + +static void filter_video(struct MPContext *mpctx, struct mp_image *frame) +{ + struct sh_video *sh_video = mpctx->sh_video; + + frame->pts = sh_video->pts; + vf_filter_frame(sh_video->vfilter, frame); + filter_output_queued_frame(mpctx); +} + + +static struct demux_packet *video_read_frame(struct MPContext *mpctx) +{ + sh_video_t *sh_video = mpctx->sh_video; + demuxer_t *demuxer = sh_video->gsh->demuxer; + float pts1 = sh_video->last_pts; + + struct demux_packet *pkt = demux_read_packet(sh_video->gsh); + if (!pkt) + return NULL; // EOF + + if (pkt->pts != MP_NOPTS_VALUE) + sh_video->last_pts = pkt->pts; + + float frame_time = sh_video->fps > 0 ? 1.0f / sh_video->fps : 0; + + // override frame_time for variable/unknown FPS formats: + if (!mpctx->opts->force_fps) { + double next_pts = demux_get_next_pts(sh_video->gsh); + double d = next_pts == MP_NOPTS_VALUE ? sh_video->last_pts - pts1 + : next_pts - sh_video->last_pts; + if (d >= 0) { + if (demuxer->type == DEMUXER_TYPE_TV) { + if (d > 0) + sh_video->fps = 1.0f / d; + frame_time = d; + } else { + if ((int)sh_video->fps <= 1) + frame_time = d; + } + } + } + + sh_video->pts = sh_video->last_pts; + sh_video->next_frame_time = frame_time; + return pkt; +} + +static double update_video_nocorrect_pts(struct MPContext *mpctx) +{ + struct sh_video *sh_video = mpctx->sh_video; + double frame_time = 0; + while (1) { + // In nocorrect-pts mode there is no way to properly time these frames + if (load_next_vo_frame(mpctx, false)) + break; + frame_time = sh_video->next_frame_time; + if (mpctx->restart_playback) + frame_time = 0; + struct demux_packet *pkt = video_read_frame(mpctx); + if (!pkt) + return -1; + if (mpctx->sh_audio) + mpctx->delay -= frame_time; + // video_read_frame can change fps (e.g. for ASF video) + update_fps(mpctx); + int framedrop_type = check_framedrop(mpctx, frame_time); + + void *decoded_frame = decode_video(sh_video, pkt, framedrop_type, + sh_video->pts); + talloc_free(pkt); + if (decoded_frame) { + filter_video(mpctx, decoded_frame); + } + break; + } + return frame_time; +} + +static double update_video_attached_pic(struct MPContext *mpctx) +{ + struct sh_video *sh_video = mpctx->sh_video; + + // Try to decode the picture multiple times, until it is displayed. + if (mpctx->video_out->hasframe) + return -1; + + struct mp_image *decoded_frame = + decode_video(sh_video, sh_video->gsh->attached_picture, 0, 0); + if (decoded_frame) + filter_video(mpctx, decoded_frame); + load_next_vo_frame(mpctx, true); + mpctx->sh_video->pts = MP_NOPTS_VALUE; + return 0; +} + +static void determine_frame_pts(struct MPContext *mpctx) +{ + struct sh_video *sh_video = mpctx->sh_video; + struct MPOpts *opts = mpctx->opts; + + if (opts->user_pts_assoc_mode) + sh_video->pts_assoc_mode = opts->user_pts_assoc_mode; + else if (sh_video->pts_assoc_mode == 0) { + if (mpctx->sh_video->gsh->demuxer->timestamp_type == TIMESTAMP_TYPE_PTS + && sh_video->codec_reordered_pts != MP_NOPTS_VALUE) + sh_video->pts_assoc_mode = 1; + else + sh_video->pts_assoc_mode = 2; + } else { + int probcount1 = sh_video->num_reordered_pts_problems; + int probcount2 = sh_video->num_sorted_pts_problems; + if (sh_video->pts_assoc_mode == 2) { + int tmp = probcount1; + probcount1 = probcount2; + probcount2 = tmp; + } + if (probcount1 >= probcount2 * 1.5 + 2) { + sh_video->pts_assoc_mode = 3 - sh_video->pts_assoc_mode; + mp_msg(MSGT_CPLAYER, MSGL_V, "Switching to pts association mode " + "%d.\n", sh_video->pts_assoc_mode); + } + } + sh_video->pts = sh_video->pts_assoc_mode == 1 ? + sh_video->codec_reordered_pts : sh_video->sorted_pts; +} + +static double update_video(struct MPContext *mpctx, double endpts) +{ + struct sh_video *sh_video = mpctx->sh_video; + struct vo *video_out = mpctx->video_out; + sh_video->vfilter->control(sh_video->vfilter, VFCTRL_SET_OSD_OBJ, + mpctx->osd); // for vf_sub + if (!mpctx->opts->correct_pts) + return update_video_nocorrect_pts(mpctx); + + if (sh_video->gsh->attached_picture) + return update_video_attached_pic(mpctx); + + double pts; + + while (1) { + if (load_next_vo_frame(mpctx, false)) + break; + pts = MP_NOPTS_VALUE; + struct demux_packet *pkt = NULL; + while (1) { + pkt = demux_read_packet(mpctx->sh_video->gsh); + if (!pkt || pkt->len) + break; + /* Packets with size 0 are assumed to not correspond to frames, + * but to indicate the absence of a frame in formats like AVI + * that must have packets at fixed timecode intervals. */ + talloc_free(pkt); + } + if (pkt) + pts = pkt->pts; + if (pts != MP_NOPTS_VALUE) + pts += mpctx->video_offset; + if (pts >= mpctx->hrseek_pts - .005) + mpctx->hrseek_framedrop = false; + int framedrop_type = mpctx->hrseek_active && mpctx->hrseek_framedrop ? + 1 : check_framedrop(mpctx, -1); + struct mp_image *decoded_frame = + decode_video(sh_video, pkt, framedrop_type, pts); + talloc_free(pkt); + if (decoded_frame) { + determine_frame_pts(mpctx); + filter_video(mpctx, decoded_frame); + } else if (!pkt) { + if (!load_next_vo_frame(mpctx, true)) + return -1; + } + break; + } + + if (!video_out->frame_loaded) + return 0; + + pts = video_out->next_pts; + if (pts == MP_NOPTS_VALUE) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "Video pts after filters MISSING\n"); + // Try to use decoder pts from before filters + pts = sh_video->pts; + if (pts == MP_NOPTS_VALUE) + pts = sh_video->last_pts; + } + if (endpts == MP_NOPTS_VALUE || pts < endpts) + add_frame_pts(mpctx, pts); + if (mpctx->hrseek_active && pts < mpctx->hrseek_pts - .005) { + vo_skip_frame(video_out); + return 0; + } + mpctx->hrseek_active = false; + sh_video->pts = pts; + if (sh_video->last_pts == MP_NOPTS_VALUE) + sh_video->last_pts = sh_video->pts; + else if (sh_video->last_pts > sh_video->pts) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "Decreasing video pts: %f < %f\n", + sh_video->pts, sh_video->last_pts); + /* If the difference in pts is small treat it as jitter around the + * right value (possibly caused by incorrect timestamp ordering) and + * just show this frame immediately after the last one. + * Treat bigger differences as timestamp resets and start counting + * timing of later frames from the position of this one. */ + if (sh_video->last_pts - sh_video->pts > 0.5) + sh_video->last_pts = sh_video->pts; + else + sh_video->pts = sh_video->last_pts; + } else if (sh_video->pts >= sh_video->last_pts + 60) { + // Assume a PTS difference >= 60 seconds is a discontinuity. + mp_msg(MSGT_CPLAYER, MSGL_WARN, "Jump in video pts: %f -> %f\n", + sh_video->last_pts, sh_video->pts); + sh_video->last_pts = sh_video->pts; + } + double frame_time = sh_video->pts - sh_video->last_pts; + sh_video->last_pts = sh_video->pts; + if (mpctx->sh_audio) + mpctx->delay -= frame_time; + return frame_time; +} + +void pause_player(struct MPContext *mpctx) +{ + mpctx->opts->pause = 1; + + if (mpctx->video_out) + vo_control(mpctx->video_out, VOCTRL_RESTORE_SCREENSAVER, NULL); + + if (mpctx->paused) + return; + mpctx->paused = true; + mpctx->step_frames = 0; + mpctx->time_frame -= get_relative_time(mpctx); + mpctx->osd_function = 0; + mpctx->paused_for_cache = false; + + if (mpctx->video_out && mpctx->sh_video && mpctx->video_out->config_ok) + vo_control(mpctx->video_out, VOCTRL_PAUSE, NULL); + + if (mpctx->ao && mpctx->sh_audio) + ao_pause(mpctx->ao); // pause audio, keep data if possible + + // Only print status if there's actually a file being played. + if (mpctx->num_sources) + print_status(mpctx); + + if (!mpctx->opts->quiet) + mp_msg(MSGT_IDENTIFY, MSGL_INFO, "ID_PAUSED\n"); +} + +void unpause_player(struct MPContext *mpctx) +{ + mpctx->opts->pause = 0; + + if (mpctx->video_out && mpctx->opts->stop_screensaver) + vo_control(mpctx->video_out, VOCTRL_KILL_SCREENSAVER, NULL); + + if (!mpctx->paused) + return; + // Don't actually unpause while cache is loading. + if (mpctx->paused_for_cache) + return; + mpctx->paused = false; + mpctx->osd_function = 0; + + if (mpctx->ao && mpctx->sh_audio) + ao_resume(mpctx->ao); + if (mpctx->video_out && mpctx->sh_video && mpctx->video_out->config_ok) + vo_control(mpctx->video_out, VOCTRL_RESUME, NULL); // resume video + (void)get_relative_time(mpctx); // ignore time that passed during pause +} + +static void draw_osd(struct MPContext *mpctx) +{ + struct vo *vo = mpctx->video_out; + + mpctx->osd->vo_pts = mpctx->video_pts; + vo_draw_osd(vo, mpctx->osd); +} + +static bool redraw_osd(struct MPContext *mpctx) +{ + struct vo *vo = mpctx->video_out; + if (vo_redraw_frame(vo) < 0) + return false; + + if (mpctx->sh_video) + update_subtitles(mpctx, mpctx->sh_video->pts); + draw_osd(mpctx); + + vo_flip_page(vo, 0, -1); + return true; +} + +void add_step_frame(struct MPContext *mpctx, int dir) +{ + if (dir > 0) { + mpctx->step_frames += 1; + unpause_player(mpctx); + } else if (dir < 0) { + if (!mpctx->backstep_active && !mpctx->hrseek_active) { + mpctx->backstep_active = true; + mpctx->backstep_start_seek_ts = mpctx->vo_pts_history_seek_ts; + pause_player(mpctx); + } + } +} + +static void seek_reset(struct MPContext *mpctx, bool reset_ao, bool reset_ac) +{ + if (mpctx->sh_video) { + resync_video_stream(mpctx->sh_video); + vo_seek_reset(mpctx->video_out); + if (mpctx->sh_video->vf_initialized == 1) + vf_chain_seek_reset(mpctx->sh_video->vfilter); + mpctx->sh_video->num_buffered_pts = 0; + mpctx->sh_video->last_pts = MP_NOPTS_VALUE; + mpctx->sh_video->pts = MP_NOPTS_VALUE; + mpctx->video_pts = MP_NOPTS_VALUE; + mpctx->delay = 0; + mpctx->time_frame = 0; + } + + if (mpctx->sh_audio && reset_ac) { + resync_audio_stream(mpctx->sh_audio); + if (reset_ao) + ao_reset(mpctx->ao); + mpctx->ao->buffer.len = mpctx->ao->buffer_playable_size; + mpctx->sh_audio->a_buffer_len = 0; + } + + reset_subtitles(mpctx); + + mpctx->restart_playback = true; + mpctx->hrseek_active = false; + mpctx->hrseek_framedrop = false; + mpctx->total_avsync_change = 0; + mpctx->drop_frame_cnt = 0; + mpctx->dropped_frames = 0; + mpctx->playback_pts = MP_NOPTS_VALUE; + +#ifdef CONFIG_ENCODING + encode_lavc_discontinuity(mpctx->encode_lavc_ctx); +#endif +} + +static bool timeline_set_part(struct MPContext *mpctx, int i, bool force) +{ + struct timeline_part *p = mpctx->timeline + mpctx->timeline_part; + struct timeline_part *n = mpctx->timeline + i; + mpctx->timeline_part = i; + mpctx->video_offset = n->start - n->source_start; + if (n->source == p->source && !force) + return false; + enum stop_play_reason orig_stop_play = mpctx->stop_play; + if (!mpctx->sh_video && mpctx->stop_play == KEEP_PLAYING) + mpctx->stop_play = AT_END_OF_FILE; // let audio uninit drain data + uninit_player(mpctx, INITIALIZED_VCODEC | (mpctx->opts->fixed_vo ? 0 : INITIALIZED_VO) | (mpctx->opts->gapless_audio ? 0 : INITIALIZED_AO) | INITIALIZED_VOL | INITIALIZED_ACODEC | INITIALIZED_SUB); + mpctx->stop_play = orig_stop_play; + + mpctx->demuxer = n->source; + mpctx->stream = mpctx->demuxer->stream; + + // While another timeline was active, the selection of active tracks might + // have been changed - possibly we need to update this source. + for (int x = 0; x < mpctx->num_tracks; x++) { + struct track *track = mpctx->tracks[x]; + if (track->under_timeline) { + track->demuxer = mpctx->demuxer; + track->stream = demuxer_stream_by_demuxer_id(track->demuxer, + track->type, + track->demuxer_id); + } + } + preselect_demux_streams(mpctx); + + return true; +} + +// Given pts, switch playback to the corresponding part. +// Return offset within that part. +static double timeline_set_from_time(struct MPContext *mpctx, double pts, + bool *need_reset) +{ + if (pts < 0) + pts = 0; + for (int i = 0; i < mpctx->num_timeline_parts; i++) { + struct timeline_part *p = mpctx->timeline + i; + if (pts < (p + 1)->start) { + *need_reset = timeline_set_part(mpctx, i, false); + return pts - p->start + p->source_start; + } + } + return -1; +} + + +// return -1 if seek failed (non-seekable stream?), 0 otherwise +static int seek(MPContext *mpctx, struct seek_params seek, + bool timeline_fallthrough) +{ + struct MPOpts *opts = mpctx->opts; + uint64_t prev_seek_ts = mpctx->vo_pts_history_seek_ts; + + if (!mpctx->demuxer) + return -1; + + if (mpctx->stop_play == AT_END_OF_FILE) + mpctx->stop_play = KEEP_PLAYING; + bool hr_seek = mpctx->demuxer->accurate_seek && opts->correct_pts; + hr_seek &= seek.exact >= 0 && seek.type != MPSEEK_FACTOR; + hr_seek &= (opts->hr_seek == 0 && seek.type == MPSEEK_ABSOLUTE) || + opts->hr_seek > 0 || seek.exact > 0; + if (seek.type == MPSEEK_FACTOR || seek.amount < 0 || + (seek.type == MPSEEK_ABSOLUTE && seek.amount < mpctx->last_chapter_pts)) + mpctx->last_chapter_seek = -2; + if (seek.type == MPSEEK_FACTOR) { + double len = get_time_length(mpctx); + if (len > 0 && !mpctx->demuxer->ts_resets_possible) { + seek.amount = seek.amount * len + get_start_time(mpctx); + seek.type = MPSEEK_ABSOLUTE; + } + } + if ((mpctx->demuxer->accurate_seek || mpctx->timeline) + && seek.type == MPSEEK_RELATIVE) { + seek.type = MPSEEK_ABSOLUTE; + seek.direction = seek.amount > 0 ? 1 : -1; + seek.amount += get_current_time(mpctx); + } + + /* At least the liba52 decoder wants to read from the input stream + * during initialization, so reinit must be done after the demux_seek() + * call that clears possible stream EOF. */ + bool need_reset = false; + double demuxer_amount = seek.amount; + if (mpctx->timeline) { + demuxer_amount = timeline_set_from_time(mpctx, seek.amount, + &need_reset); + if (demuxer_amount == -1) { + assert(!need_reset); + mpctx->stop_play = AT_END_OF_FILE; + // Clear audio from current position + if (mpctx->sh_audio && !timeline_fallthrough) { + ao_reset(mpctx->ao); + mpctx->sh_audio->a_buffer_len = 0; + } + return -1; + } + } + if (need_reset) { + reinit_video_chain(mpctx); + reinit_subs(mpctx); + } + + int demuxer_style = 0; + switch (seek.type) { + case MPSEEK_FACTOR: + demuxer_style |= SEEK_ABSOLUTE | SEEK_FACTOR; + break; + case MPSEEK_ABSOLUTE: + demuxer_style |= SEEK_ABSOLUTE; + break; + } + if (hr_seek || seek.direction < 0) + demuxer_style |= SEEK_BACKWARD; + else if (seek.direction > 0) + demuxer_style |= SEEK_FORWARD; + if (hr_seek || opts->mkv_subtitle_preroll) + demuxer_style |= SEEK_SUBPREROLL; + + if (hr_seek) + demuxer_amount -= opts->hr_seek_demuxer_offset; + int seekresult = demux_seek(mpctx->demuxer, demuxer_amount, + mpctx->audio_delay, demuxer_style); + if (seekresult == 0) { + if (need_reset) { + reinit_audio_chain(mpctx); + seek_reset(mpctx, !timeline_fallthrough, false); + } + return -1; + } + + // If audio or demuxer subs come from different files, seek them too: + bool have_external_tracks = false; + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + struct track *track = mpctx->current_track[type]; + have_external_tracks |= track && track->is_external && track->demuxer; + } + if (have_external_tracks) { + double main_new_pos; + if (seek.type == MPSEEK_ABSOLUTE) { + main_new_pos = seek.amount - mpctx->video_offset; + } else { + main_new_pos = get_main_demux_pts(mpctx); + } + for (int type = 0; type < STREAM_TYPE_COUNT; type++) { + struct track *track = mpctx->current_track[type]; + if (track && track->is_external && track->demuxer) + demux_seek(track->demuxer, main_new_pos, mpctx->audio_delay, + SEEK_ABSOLUTE); + } + } + + if (need_reset) + reinit_audio_chain(mpctx); + /* If we just reinitialized audio it doesn't need to be reset, + * and resetting could lose audio some decoders produce during init. */ + seek_reset(mpctx, !timeline_fallthrough, !need_reset); + + if (timeline_fallthrough) { + // Important if video reinit happens. + mpctx->vo_pts_history_seek_ts = prev_seek_ts; + } else { + mpctx->vo_pts_history_seek_ts++; + mpctx->backstep_active = false; + } + + /* Use the target time as "current position" for further relative + * seeks etc until a new video frame has been decoded */ + if (seek.type == MPSEEK_ABSOLUTE) { + mpctx->video_pts = seek.amount; + mpctx->last_seek_pts = seek.amount; + } else + mpctx->last_seek_pts = MP_NOPTS_VALUE; + + // The hr_seek==false case is for skipping frames with PTS before the + // current timeline chapter start. It's not really known where the demuxer + // level seek will end up, so the hrseek mechanism is abused to skip all + // frames before chapter start by setting hrseek_pts to the chapter start. + // It does nothing when the seek is inside of the current chapter, and + // seeking past the chapter is handled elsewhere. + if (hr_seek || mpctx->timeline) { + mpctx->hrseek_active = true; + mpctx->hrseek_framedrop = true; + mpctx->hrseek_pts = hr_seek ? seek.amount + : mpctx->timeline[mpctx->timeline_part].start; + } + + mpctx->start_timestamp = mp_time_sec(); + + return 0; +} + +void queue_seek(struct MPContext *mpctx, enum seek_type type, double amount, + int exact) +{ + struct seek_params *seek = &mpctx->seek; + switch (type) { + case MPSEEK_RELATIVE: + if (seek->type == MPSEEK_FACTOR) + return; // Well... not common enough to bother doing better + seek->amount += amount; + seek->exact = FFMAX(seek->exact, exact); + if (seek->type == MPSEEK_NONE) + seek->exact = exact; + if (seek->type == MPSEEK_ABSOLUTE) + return; + if (seek->amount == 0) { + *seek = (struct seek_params){ 0 }; + return; + } + seek->type = MPSEEK_RELATIVE; + return; + case MPSEEK_ABSOLUTE: + case MPSEEK_FACTOR: + *seek = (struct seek_params) { + .type = type, + .amount = amount, + .exact = exact, + }; + return; + case MPSEEK_NONE: + *seek = (struct seek_params){ 0 }; + return; + } + abort(); +} + +static void execute_queued_seek(struct MPContext *mpctx) +{ + if (mpctx->seek.type) { + seek(mpctx, mpctx->seek, false); + mpctx->seek = (struct seek_params){0}; + } +} + +double get_time_length(struct MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer) + return 0; + + if (mpctx->timeline) + return mpctx->timeline[mpctx->num_timeline_parts].start; + + double len = demuxer_get_time_length(demuxer); + if (len >= 0) + return len; + + // Unknown + return 0; +} + +/* If there are timestamps from stream level then use those (for example + * DVDs can have consistent times there while the MPEG-level timestamps + * reset). */ +double get_current_time(struct MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer) + return 0; + if (demuxer->stream_pts != MP_NOPTS_VALUE) + return demuxer->stream_pts; + if (mpctx->playback_pts != MP_NOPTS_VALUE) + return mpctx->playback_pts; + if (mpctx->last_seek_pts != MP_NOPTS_VALUE) + return mpctx->last_seek_pts; + return 0; +} + +double get_start_time(struct MPContext *mpctx) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer) + return 0; + return demuxer_get_start_time(demuxer); +} + +// Return playback position in 0.0-1.0 ratio, or -1 if unknown. +double get_current_pos_ratio(struct MPContext *mpctx, bool use_range) +{ + struct demuxer *demuxer = mpctx->demuxer; + if (!demuxer) + return -1; + double ans = -1; + double start = get_start_time(mpctx); + double len = get_time_length(mpctx); + if (use_range) { + double startpos = rel_time_to_abs(mpctx, mpctx->opts->play_start, + MP_NOPTS_VALUE); + double endpos = get_play_end_pts(mpctx); + if (endpos == MP_NOPTS_VALUE || endpos > start + len) + endpos = start + len; + if (startpos == MP_NOPTS_VALUE || startpos < start) + startpos = start; + if (endpos < startpos) + endpos = startpos; + start = startpos; + len = endpos - startpos; + } + double pos = get_current_time(mpctx); + if (len > 0 && !demuxer->ts_resets_possible) { + ans = av_clipf((pos - start) / len, 0, 1); + } else { + int64_t size = (demuxer->movi_end - demuxer->movi_start); + int64_t fpos = demuxer->filepos > 0 ? + demuxer->filepos : stream_tell(demuxer->stream); + if (size > 0) + ans = av_clipf((double)(fpos - demuxer->movi_start) / size, 0, 1); + } + if (use_range) { + if (mpctx->opts->play_frames > 0) + ans = max(ans, 1.0 - + mpctx->max_frames / (double) mpctx->opts->play_frames); + } + return ans; +} + +int get_percent_pos(struct MPContext *mpctx) +{ + return av_clip(get_current_pos_ratio(mpctx, false) * 100, 0, 100); +} + +// -2 is no chapters, -1 is before first chapter +int get_current_chapter(struct MPContext *mpctx) +{ + double current_pts = get_current_time(mpctx); + if (mpctx->chapters) { + int i; + for (i = 1; i < mpctx->num_chapters; i++) + if (current_pts < mpctx->chapters[i].start) + break; + return FFMAX(mpctx->last_chapter_seek, i - 1); + } + if (mpctx->master_demuxer) + return FFMAX(mpctx->last_chapter_seek, + demuxer_get_current_chapter(mpctx->master_demuxer, current_pts)); + return -2; +} + +char *chapter_display_name(struct MPContext *mpctx, int chapter) +{ + char *name = chapter_name(mpctx, chapter); + char *dname = name; + if (name) { + dname = talloc_asprintf(NULL, "(%d) %s", chapter + 1, name); + } else if (chapter < -1) { + dname = talloc_strdup(NULL, "(unavailable)"); + } else { + int chapter_count = get_chapter_count(mpctx); + if (chapter_count <= 0) + dname = talloc_asprintf(NULL, "(%d)", chapter + 1); + else + dname = talloc_asprintf(NULL, "(%d) of %d", chapter + 1, + chapter_count); + } + if (dname != name) + talloc_free(name); + return dname; +} + +// returns NULL if chapter name unavailable +char *chapter_name(struct MPContext *mpctx, int chapter) +{ + if (mpctx->chapters) { + if (chapter < 0 || chapter >= mpctx->num_chapters) + return NULL; + return talloc_strdup(NULL, mpctx->chapters[chapter].name); + } + if (mpctx->master_demuxer) + return demuxer_chapter_name(mpctx->master_demuxer, chapter); + return NULL; +} + +// returns the start of the chapter in seconds (-1 if unavailable) +double chapter_start_time(struct MPContext *mpctx, int chapter) +{ + if (mpctx->chapters) + return mpctx->chapters[chapter].start; + if (mpctx->master_demuxer) + return demuxer_chapter_time(mpctx->master_demuxer, chapter); + return -1; +} + +int get_chapter_count(struct MPContext *mpctx) +{ + if (mpctx->chapters) + return mpctx->num_chapters; + if (mpctx->master_demuxer) + return demuxer_chapter_count(mpctx->master_demuxer); + return 0; +} + +// Seek to a given chapter. Tries to queue the seek, but might seek immediately +// in some cases. Returns success, no matter if seek is queued or immediate. +bool mp_seek_chapter(struct MPContext *mpctx, int chapter) +{ + int num = get_chapter_count(mpctx); + if (num == 0) + return false; + if (chapter < 0 || chapter >= num) + return false; + + mpctx->last_chapter_seek = -2; + + double pts; + if (mpctx->chapters) { + pts = mpctx->chapters[chapter].start; + goto do_seek; + } else if (mpctx->master_demuxer) { + int res = demuxer_seek_chapter(mpctx->master_demuxer, chapter, &pts); + if (res >= 0) { + if (pts == -1) { + // for DVD/BD - seek happened via stream layer + seek_reset(mpctx, true, true); + mpctx->seek = (struct seek_params){0}; + return true; + } + chapter = res; + goto do_seek; + } + } + return false; + +do_seek: + queue_seek(mpctx, MPSEEK_ABSOLUTE, pts, 0); + mpctx->last_chapter_seek = chapter; + mpctx->last_chapter_pts = pts; + return true; +} + +static void update_avsync(struct MPContext *mpctx) +{ + if (!mpctx->sh_audio || !mpctx->sh_video) + return; + + double a_pos = playing_audio_pts(mpctx); + + mpctx->last_av_difference = a_pos - mpctx->video_pts - mpctx->audio_delay; + if (mpctx->time_frame > 0) + mpctx->last_av_difference += + mpctx->time_frame * mpctx->opts->playback_speed; + if (a_pos == MP_NOPTS_VALUE || mpctx->video_pts == MP_NOPTS_VALUE) + mpctx->last_av_difference = MP_NOPTS_VALUE; + if (mpctx->last_av_difference > 0.5 && mpctx->drop_frame_cnt > 50 + && !mpctx->drop_message_shown) { + mp_tmsg(MSGT_AVSYNC, MSGL_WARN, "%s", mp_gtext(av_desync_help_text)); + mpctx->drop_message_shown = true; + } +} + +static void handle_pause_on_low_cache(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + int cache = mp_get_cache_percent(mpctx); + bool idle = mp_get_cache_idle(mpctx); + if (mpctx->paused && mpctx->paused_for_cache) { + if (cache < 0 || cache >= opts->stream_cache_min_percent || idle) { + mpctx->paused_for_cache = false; + if (!opts->pause) + unpause_player(mpctx); + } + } else { + if (cache >= 0 && cache <= opts->stream_cache_pause && !idle) { + bool prev_paused_user = opts->pause; + pause_player(mpctx); + mpctx->paused_for_cache = true; + opts->pause = prev_paused_user; + } + } +} + +static double get_wakeup_period(struct MPContext *mpctx) +{ + /* Even if we can immediately wake up in response to most input events, + * there are some timers which are not registered to the event loop + * and need to be checked periodically (like automatic mouse cursor hiding). + * OSD content updates behave similarly. Also some uncommon input devices + * may not have proper FD event support. + */ + double sleeptime = WAKEUP_PERIOD; + +#ifndef HAVE_POSIX_SELECT + // No proper file descriptor event handling; keep waking up to poll input + sleeptime = FFMIN(sleeptime, 0.02); +#endif + + if (mpctx->video_out) + if (mpctx->video_out->wakeup_period > 0) + sleeptime = FFMIN(sleeptime, mpctx->video_out->wakeup_period); + + return sleeptime; +} + +static void run_playloop(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + bool full_audio_buffers = false; + bool audio_left = false, video_left = false; + double endpts = get_play_end_pts(mpctx); + bool end_is_chapter = false; + double sleeptime = get_wakeup_period(mpctx); + bool was_restart = mpctx->restart_playback; + bool new_frame_shown = false; + +#ifdef CONFIG_ENCODING + if (encode_lavc_didfail(mpctx->encode_lavc_ctx)) { + mpctx->stop_play = PT_QUIT; + return; + } +#endif + + // Add tracks that were added by the demuxer later (e.g. MPEG) + if (!mpctx->timeline && mpctx->demuxer) + add_demuxer_tracks(mpctx, mpctx->demuxer); + + if (mpctx->timeline) { + double end = mpctx->timeline[mpctx->timeline_part + 1].start; + if (endpts == MP_NOPTS_VALUE || end < endpts) { + endpts = end; + end_is_chapter = true; + } + } + + if (opts->chapterrange[1] > 0) { + int cur_chapter = get_current_chapter(mpctx); + if (cur_chapter != -1 && cur_chapter + 1 > opts->chapterrange[1]) + mpctx->stop_play = PT_NEXT_ENTRY; + } + + if (mpctx->sh_audio && !mpctx->restart_playback && !mpctx->ao->untimed) { + int status = fill_audio_out_buffers(mpctx, endpts); + full_audio_buffers = status >= 0; + // Not at audio stream EOF yet + audio_left = status > -2; + } + + double buffered_audio = -1; + while (mpctx->sh_video) { // never loops, for "break;" only + struct vo *vo = mpctx->video_out; + update_fps(mpctx); + + video_left = vo->hasframe || vo->frame_loaded; + if (!vo->frame_loaded && (!mpctx->paused || mpctx->restart_playback)) { + double frame_time = update_video(mpctx, endpts); + mp_dbg(MSGT_AVSYNC, MSGL_DBG2, "*** ftime=%5.3f ***\n", frame_time); + if (mpctx->sh_video->vf_initialized < 0) { + mp_tmsg(MSGT_CPLAYER, MSGL_FATAL, + "\nFATAL: Could not initialize video filters (-vf) " + "or video output (-vo).\n"); + uninit_player(mpctx, INITIALIZED_VCODEC | INITIALIZED_VO); + cleanup_demux_stream(mpctx, STREAM_VIDEO); + mpctx->current_track[STREAM_VIDEO] = NULL; + if (!mpctx->current_track[STREAM_AUDIO]) + mpctx->stop_play = PT_NEXT_ENTRY; + mpctx->error_playing = true; + break; + } + video_left = frame_time >= 0; + if (video_left && !mpctx->restart_playback) { + mpctx->time_frame += frame_time / opts->playback_speed; + adjust_sync(mpctx, frame_time); + } + if (!video_left) { + mpctx->delay = 0; + mpctx->last_av_difference = 0; + } + } + + if (endpts != MP_NOPTS_VALUE) + video_left &= mpctx->sh_video->pts < endpts; + + // ================================================================ + vo_check_events(vo); + + unsigned mouse_event_ts = mp_input_get_mouse_event_counter(mpctx->input); + if (mpctx->mouse_event_ts != mouse_event_ts) { + mpctx->mouse_event_ts = mouse_event_ts; + if (opts->vo.cursor_autohide_delay > -1) { + vo_control(vo, VOCTRL_SET_CURSOR_VISIBILITY, &(bool){true}); + if (opts->vo.cursor_autohide_delay >= 0) { + mpctx->mouse_waiting_hide = 1; + mpctx->mouse_timer = + mp_time_sec() + opts->vo.cursor_autohide_delay / 1000.0; + } + } + } + + if (mpctx->mouse_waiting_hide == 1 && + mp_time_sec() >= mpctx->mouse_timer) + { + vo_control(vo, VOCTRL_SET_CURSOR_VISIBILITY, &(bool){false}); + mpctx->mouse_waiting_hide = 2; + } + + if (opts->heartbeat_cmd) { + double now = mp_time_sec(); + if (now - mpctx->last_heartbeat > opts->heartbeat_interval) { + mpctx->last_heartbeat = now; + system(opts->heartbeat_cmd); + } + } + + if (!video_left || (mpctx->paused && !mpctx->restart_playback)) + break; + if (!vo->frame_loaded) { + sleeptime = 0; + break; + } + + mpctx->time_frame -= get_relative_time(mpctx); + if (full_audio_buffers && !mpctx->restart_playback) { + buffered_audio = ao_get_delay(mpctx->ao); + mp_dbg(MSGT_AVSYNC, MSGL_DBG2, "delay=%f\n", buffered_audio); + + if (opts->autosync) { + /* Smooth reported playback position from AO by averaging + * it with the value expected based on previus value and + * time elapsed since then. May help smooth video timing + * with audio output that have inaccurate position reporting. + * This is badly implemented; the behavior of the smoothing + * now undesirably depends on how often this code runs + * (mainly depends on video frame rate). */ + float predicted = (mpctx->delay / opts->playback_speed + + mpctx->time_frame); + float difference = buffered_audio - predicted; + buffered_audio = predicted + difference / opts->autosync; + } + + mpctx->time_frame = (buffered_audio - + mpctx->delay / opts->playback_speed); + } else { + /* If we're more than 200 ms behind the right playback + * position, don't try to speed up display of following + * frames to catch up; continue with default speed from + * the current frame instead. + * If untimed is set always output frames immediately + * without sleeping. + */ + if (mpctx->time_frame < -0.2 || opts->untimed || vo->untimed) + mpctx->time_frame = 0; + } + + double vsleep = mpctx->time_frame - vo->flip_queue_offset; + if (vsleep > 0.050) { + sleeptime = FFMIN(sleeptime, vsleep - 0.040); + break; + } + sleeptime = 0; + + //=================== FLIP PAGE (VIDEO BLT): ====================== + + vo_new_frame_imminent(vo); + struct sh_video *sh_video = mpctx->sh_video; + mpctx->video_pts = sh_video->pts; + mpctx->last_vo_pts = mpctx->video_pts; + mpctx->playback_pts = mpctx->video_pts; + update_subtitles(mpctx, sh_video->pts); + update_osd_msg(mpctx); + draw_osd(mpctx); + + mpctx->time_frame -= get_relative_time(mpctx); + mpctx->time_frame -= vo->flip_queue_offset; + if (mpctx->time_frame > 0.001) + mpctx->time_frame = timing_sleep(mpctx, mpctx->time_frame); + mpctx->time_frame += vo->flip_queue_offset; + + int64_t t2 = mp_time_us(); + /* Playing with playback speed it's possible to get pathological + * cases with mpctx->time_frame negative enough to cause an + * overflow in pts_us calculation, thus the FFMAX. */ + double time_frame = FFMAX(mpctx->time_frame, -1); + int64_t pts_us = mpctx->last_time + time_frame * 1e6; + int duration = -1; + double pts2 = vo->next_pts2; + if (pts2 != MP_NOPTS_VALUE && opts->correct_pts && + !mpctx->restart_playback) { + // expected A/V sync correction is ignored + double diff = (pts2 - mpctx->video_pts); + diff /= opts->playback_speed; + if (mpctx->time_frame < 0) + diff += mpctx->time_frame; + if (diff < 0) + diff = 0; + if (diff > 10) + diff = 10; + duration = diff * 1e6; + } + vo_flip_page(vo, pts_us | 1, duration); + + mpctx->last_vo_flip_duration = (mp_time_us() - t2) * 0.000001; + if (vo->driver->flip_page_timed) { + // No need to adjust sync based on flip speed + mpctx->last_vo_flip_duration = 0; + // For print_status - VO call finishing early is OK for sync + mpctx->time_frame -= get_relative_time(mpctx); + } + if (mpctx->restart_playback) { + if (mpctx->sync_audio_to_video) { + mpctx->syncing_audio = true; + if (mpctx->sh_audio) + fill_audio_out_buffers(mpctx, endpts); + mpctx->restart_playback = false; + } + mpctx->time_frame = 0; + get_relative_time(mpctx); + } + update_avsync(mpctx); + print_status(mpctx); + screenshot_flip(mpctx); + new_frame_shown = true; + + break; + } // video + + video_left &= mpctx->sync_audio_to_video; // force no-video semantics + + if (mpctx->sh_audio && (mpctx->restart_playback ? !video_left : + mpctx->ao->untimed && (mpctx->delay <= 0 || + !video_left))) { + int status = fill_audio_out_buffers(mpctx, endpts); + full_audio_buffers = status >= 0 && !mpctx->ao->untimed; + // Not at audio stream EOF yet + audio_left = status > -2; + } + if (!video_left) + mpctx->restart_playback = false; + if (mpctx->sh_audio && buffered_audio == -1) + buffered_audio = mpctx->paused ? 0 : ao_get_delay(mpctx->ao); + + update_osd_msg(mpctx); + + // The cache status is part of the status line. Possibly update it. + if (mpctx->paused && mp_get_cache_percent(mpctx) >= 0) + print_status(mpctx); + + if (!video_left && (!mpctx->paused || was_restart)) { + double a_pos = 0; + if (mpctx->sh_audio) { + a_pos = (written_audio_pts(mpctx) - + mpctx->opts->playback_speed * buffered_audio); + } + mpctx->playback_pts = a_pos; + print_status(mpctx); + + if (!mpctx->sh_video) + update_subtitles(mpctx, a_pos); + } + + /* It's possible for the user to simultaneously switch both audio + * and video streams to "disabled" at runtime. Handle this by waiting + * rather than immediately stopping playback due to EOF. + * + * When all audio has been written to output driver, stay in the + * main loop handling commands until it has been mostly consumed, + * except in the gapless case, where the next file will be started + * while audio from the current one still remains to be played. + * + * We want this check to trigger if we seeked to this position, + * but not if we paused at it with audio possibly still buffered in + * the AO. There's currently no working way to check buffered audio + * inside AO while paused. Thus the "was_restart" check below, which + * should trigger after seek only, when we know there's no audio + * buffered. + */ + if ((mpctx->sh_audio || mpctx->sh_video) && !audio_left && !video_left + && (opts->gapless_audio || buffered_audio < 0.05) + && (!mpctx->paused || was_restart)) { + if (end_is_chapter) { + seek(mpctx, (struct seek_params){ + .type = MPSEEK_ABSOLUTE, + .amount = mpctx->timeline[mpctx->timeline_part+1].start + }, true); + } else + mpctx->stop_play = AT_END_OF_FILE; + sleeptime = 0; + } + + if (!mpctx->stop_play && !mpctx->restart_playback) { + + // If no more video is available, one frame means one playloop iteration. + // Otherwise, one frame means one video frame. + if (!video_left) + new_frame_shown = true; + + if (opts->playing_msg && !mpctx->playing_msg_shown && new_frame_shown) { + mpctx->playing_msg_shown = true; + char *msg = mp_property_expand_string(mpctx, opts->playing_msg); + mp_msg(MSGT_CPLAYER, MSGL_INFO, "%s\n", msg); + talloc_free(msg); + } + + if (mpctx->max_frames >= 0) { + if (new_frame_shown) + mpctx->max_frames--; + if (mpctx->max_frames <= 0) + mpctx->stop_play = PT_NEXT_ENTRY; + } + + if (mpctx->step_frames > 0 && !mpctx->paused) { + if (new_frame_shown) + mpctx->step_frames--; + if (mpctx->step_frames == 0) + pause_player(mpctx); + } + + } + + if (!mpctx->stop_play) { + double audio_sleep = 9; + if (mpctx->sh_audio && !mpctx->paused) { + if (mpctx->ao->untimed) { + if (!video_left) + audio_sleep = 0; + } else if (full_audio_buffers) { + audio_sleep = buffered_audio - 0.050; + // Keep extra safety margin if the buffers are large + if (audio_sleep > 0.100) + audio_sleep = FFMAX(audio_sleep - 0.200, 0.100); + else + audio_sleep = FFMAX(audio_sleep, 0.020); + } else + audio_sleep = 0.020; + } + sleeptime = FFMIN(sleeptime, audio_sleep); + if (sleeptime > 0 && mpctx->sh_video) { + bool want_redraw = vo_get_want_redraw(mpctx->video_out); + if (mpctx->video_out->driver->draw_osd) + want_redraw |= mpctx->osd->want_redraw; + mpctx->osd->want_redraw = false; + if (want_redraw) { + if (redraw_osd(mpctx)) { + sleeptime = 0; + } else if (mpctx->paused && video_left) { + // force redrawing OSD by framestepping + add_step_frame(mpctx, 1); + sleeptime = 0; + } + } + } + if (sleeptime > 0) + mp_input_get_cmd(mpctx->input, sleeptime * 1000, true); + } + + if (mp_time_sec() > mpctx->last_metadata_update + 2) { + demux_info_update(mpctx->demuxer); + mpctx->last_metadata_update = mp_time_sec(); + } + + //================= Keyboard events, SEEKing ==================== + + handle_pause_on_low_cache(mpctx); + + mp_cmd_t *cmd; + while ((cmd = mp_input_get_cmd(mpctx->input, 0, 1)) != NULL) { + /* Allow running consecutive seek commands to combine them, + * but execute the seek before running other commands. + * If the user seeks continuously (keeps arrow key down) + * try to finish showing a frame from one location before doing + * another seek (which could lead to unchanging display). */ + if ((mpctx->seek.type && cmd->id != MP_CMD_SEEK) || + (mpctx->restart_playback && cmd->id == MP_CMD_SEEK && + mp_time_sec() - mpctx->start_timestamp < 0.3)) + break; + cmd = mp_input_get_cmd(mpctx->input, 0, 0); + run_command(mpctx, cmd); + mp_cmd_free(cmd); + if (mpctx->stop_play) + break; + } + + if (mpctx->backstep_active) { + double current_pts = mpctx->last_vo_pts; + mpctx->backstep_active = false; + bool demuxer_ok = mpctx->demuxer && mpctx->demuxer->accurate_seek; + if (demuxer_ok && mpctx->sh_video && current_pts != MP_NOPTS_VALUE) { + double seek_pts = find_previous_pts(mpctx, current_pts); + if (seek_pts != MP_NOPTS_VALUE) { + queue_seek(mpctx, MPSEEK_ABSOLUTE, seek_pts, 1); + } else { + double last = get_last_frame_pts(mpctx); + if (last != MP_NOPTS_VALUE && last >= current_pts && + mpctx->backstep_start_seek_ts != mpctx->vo_pts_history_seek_ts) + { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "Backstep failed.\n"); + queue_seek(mpctx, MPSEEK_ABSOLUTE, current_pts, 1); + } else if (!mpctx->hrseek_active) { + mp_msg(MSGT_CPLAYER, MSGL_V, "Start backstep indexing.\n"); + // Force it to index the video up until current_pts. + // The whole point is getting frames _before_ that PTS, + // so apply an arbitrary offset. (In theory the offset + // has to be large enough to reach the previous frame.) + seek(mpctx, (struct seek_params){ + .type = MPSEEK_ABSOLUTE, + .amount = current_pts - 1.0, + }, false); + // Don't leave hr-seek mode. If all goes right, hr-seek + // mode is cancelled as soon as the frame before + // current_pts is found during hr-seeking. + // Note that current_pts should be part of the index, + // otherwise we can't find the previous frame, so set the + // seek target an arbitrary amount of time after it. + if (mpctx->hrseek_active) { + mpctx->hrseek_pts = current_pts + 10.0; + mpctx->hrseek_framedrop = false; + mpctx->backstep_active = true; + } + } else { + mpctx->backstep_active = true; + } + } + } + } + + // handle -sstep + if (opts->step_sec > 0 && !mpctx->stop_play && !mpctx->paused && + !mpctx->restart_playback) + { + set_osd_function(mpctx, OSD_FFW); + queue_seek(mpctx, MPSEEK_RELATIVE, opts->step_sec, 0); + } + + if (opts->keep_open && mpctx->stop_play == AT_END_OF_FILE) { + mpctx->stop_play = KEEP_PLAYING; + pause_player(mpctx); + if (mpctx->video_out && !mpctx->video_out->hasframe) { + // Force screen refresh to make OSD usable + double seek_to = mpctx->last_vo_pts; + if (seek_to == MP_NOPTS_VALUE) + seek_to = 0; // arbitrary default + queue_seek(mpctx, MPSEEK_ABSOLUTE, seek_to, 1); + } + } + + execute_queued_seek(mpctx); +} + +static bool attachment_is_font(struct demux_attachment *att) +{ + if (!att->name || !att->type || !att->data || !att->data_size) + return false; + // match against MIME types + if (strcmp(att->type, "application/x-truetype-font") == 0 + || strcmp(att->type, "application/x-font") == 0) + return true; + // fallback: match against file extension + if (strlen(att->name) > 4) { + char *ext = att->name + strlen(att->name) - 4; + if (strcasecmp(ext, ".ttf") == 0 || strcasecmp(ext, ".ttc") == 0 + || strcasecmp(ext, ".otf") == 0) + return true; + } + return false; +} + +// Result numerically higher => better match. 0 == no match. +static int match_lang(char **langs, char *lang) +{ + for (int idx = 0; langs && langs[idx]; idx++) { + if (lang && strcmp(langs[idx], lang) == 0) + return INT_MAX - idx; + } + return 0; +} + +/* Get the track wanted by the user. + * tid is the track ID requested by the user (-2: deselect, -1: default) + * lang is a string list, NULL is same as empty list + * Sort tracks based on the following criteria, and pick the first: + * 0) track matches tid (always wins) + * 1) track is external + * 1b) track was passed explicitly (is not an auto-loaded subtitle) + * 2) earlier match in lang list + * 3) track is marked default + * 4) lower track number + * If select_fallback is not set, 4) is only used to determine whether a + * matching track is preferred over another track. Otherwise, always pick a + * track (if nothing else matches, return the track with lowest ID). + */ +// Return whether t1 is preferred over t2 +static bool compare_track(struct track *t1, struct track *t2, char **langs) +{ + if (t1->is_external != t2->is_external) + return t1->is_external; + if (t1->auto_loaded != t2->auto_loaded) + return !t1->auto_loaded; + int l1 = match_lang(langs, t1->lang), l2 = match_lang(langs, t2->lang); + if (l1 != l2) + return l1 > l2; + if (t1->default_track != t2->default_track) + return t1->default_track; + if (t1->attached_picture != t2->attached_picture) + return !t1->attached_picture; + return t1->user_tid <= t2->user_tid; +} +static struct track *select_track(struct MPContext *mpctx, + enum stream_type type, int tid, char **langs) +{ + if (tid == -2) + return NULL; + bool select_fallback = type == STREAM_VIDEO || type == STREAM_AUDIO; + struct track *pick = NULL; + for (int n = 0; n < mpctx->num_tracks; n++) { + struct track *track = mpctx->tracks[n]; + if (track->type != type) + continue; + if (track->user_tid == tid) + return track; + if (!pick || compare_track(track, pick, langs)) + pick = track; + } + if (pick && !select_fallback && !pick->is_external + && !match_lang(langs, pick->lang) && !pick->default_track) + pick = NULL; + if (pick && pick->attached_picture && !mpctx->opts->audio_display) + pick = NULL; + return pick; +} + +// Normally, video/audio/sub track selection is persistent across files. This +// code resets track selection if the new file has a different track layout. +static void check_previous_track_selection(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + + if (!mpctx->track_layout_hash) + return; + + char *h = track_layout_hash(mpctx); + if (strcmp(h, mpctx->track_layout_hash) != 0) { + // Reset selection, but only if they're not "auto" or "off". + if (opts->video_id >= 0) + mpctx->opts->video_id = -1; + if (opts->audio_id >= 0) + mpctx->opts->audio_id = -1; + if (opts->sub_id >= 0) + mpctx->opts->sub_id = -1; + talloc_free(mpctx->track_layout_hash); + mpctx->track_layout_hash = NULL; + } + talloc_free(h); +} + +static int read_keys(void *ctx, int fd) +{ + if (getch2(ctx)) + return MP_INPUT_NOTHING; + return MP_INPUT_DEAD; +} + +static void init_input(struct MPContext *mpctx) +{ + mpctx->input = mp_input_init(mpctx->opts); + if (mpctx->opts->slave_mode) + mp_input_add_cmd_fd(mpctx->input, 0, USE_FD0_CMD_SELECT, MP_INPUT_SLAVE_CMD_FUNC, NULL); + else if (mpctx->opts->consolecontrols) + mp_input_add_key_fd(mpctx->input, 0, 1, read_keys, NULL, mpctx->input); + // Set the libstream interrupt callback + stream_set_interrupt_callback(mp_input_check_interrupt, mpctx->input); + +#ifdef CONFIG_COCOA + cocoa_set_input_context(mpctx->input); +#endif +} + +static void open_subtitles_from_options(struct MPContext *mpctx) +{ + // after reading video params we should load subtitles because + // we know fps so now we can adjust subtitle time to ~6 seconds AST + // check .sub + if (mpctx->opts->sub_name) { + for (int i = 0; mpctx->opts->sub_name[i] != NULL; ++i) + mp_add_subtitles(mpctx, mpctx->opts->sub_name[i], 0); + } + if (mpctx->opts->sub_auto) { // auto load sub file ... + char **tmp = find_text_subtitles(mpctx->opts, mpctx->filename); + int nsub = MP_TALLOC_ELEMS(tmp); + for (int i = 0; i < nsub; i++) { + struct track *track = mp_add_subtitles(mpctx, tmp[i], 1); + if (track) + track->auto_loaded = true; + } + talloc_free(tmp); + } +} + +static struct track *open_external_file(struct MPContext *mpctx, char *filename, + char *demuxer_name, int stream_cache, + enum stream_type filter) +{ + struct MPOpts *opts = mpctx->opts; + if (!filename) + return NULL; + char *disp_filename = filename; + if (strncmp(disp_filename, "memory://", 9) == 0) + disp_filename = "memory://"; // avoid noise + struct stream *stream = stream_open(filename, mpctx->opts); + if (!stream) + goto err_out; + stream_enable_cache_percent(&stream, stream_cache, + opts->stream_cache_def_size, + opts->stream_cache_min_percent, + opts->stream_cache_seek_min_percent); + struct demuxer_params params = { + .ass_library = mpctx->ass_library, // demux_libass requires it + }; + struct demuxer *demuxer = + demux_open(stream, demuxer_name, ¶ms, mpctx->opts); + if (!demuxer) { + free_stream(stream); + goto err_out; + } + struct track *first = NULL; + for (int n = 0; n < demuxer->num_streams; n++) { + struct sh_stream *sh = demuxer->streams[n]; + if (sh->type == filter) { + struct track *t = add_stream_track(mpctx, sh, false); + t->is_external = true; + t->title = talloc_strdup(t, disp_filename); + t->external_filename = talloc_strdup(t, filename); + first = t; + } + } + if (!first) { + free_demuxer(demuxer); + free_stream(stream); + mp_msg(MSGT_CPLAYER, MSGL_WARN, "No streams added from file %s.\n", + disp_filename); + goto err_out; + } + MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, demuxer); + return first; + +err_out: + mp_msg(MSGT_CPLAYER, MSGL_ERR, "Can not open external file %s.\n", + disp_filename); + return false; +} + +static void open_audiofiles_from_options(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + open_external_file(mpctx, opts->audio_stream, opts->audio_demuxer_name, + opts->audio_stream_cache, STREAM_AUDIO); +} + +struct track *mp_add_subtitles(struct MPContext *mpctx, char *filename, int noerr) +{ + struct MPOpts *opts = mpctx->opts; + return open_external_file(mpctx, filename, opts->sub_demuxer_name, 0, + STREAM_SUB); +} + +static void open_subtitles_from_resolve(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + struct mp_resolve_result *res = mpctx->resolve_result; + if (!res) + return; + for (int n = 0; n < res->num_subs; n++) { + struct mp_resolve_sub *sub = res->subs[n]; + char *s = talloc_strdup(NULL, sub->url); + if (!s) + s = talloc_asprintf(NULL, "memory://%s", sub->data); + struct track *t = + open_external_file(mpctx, s, opts->sub_demuxer_name, 0, STREAM_SUB); + talloc_free(s); + if (t) + t->lang = talloc_strdup(t, sub->lang); + } +} + +static void print_timeline(struct MPContext *mpctx) +{ + if (mpctx->timeline) { + int part_count = mpctx->num_timeline_parts; + mp_msg(MSGT_CPLAYER, MSGL_V, "Timeline contains %d parts from %d " + "sources. Total length %.3f seconds.\n", part_count, + mpctx->num_sources, mpctx->timeline[part_count].start); + mp_msg(MSGT_CPLAYER, MSGL_V, "Source files:\n"); + for (int i = 0; i < mpctx->num_sources; i++) + mp_msg(MSGT_CPLAYER, MSGL_V, "%d: %s\n", i, + mpctx->sources[i]->filename); + mp_msg(MSGT_CPLAYER, MSGL_V, "Timeline parts: (number, start, " + "source_start, source):\n"); + for (int i = 0; i < part_count; i++) { + struct timeline_part *p = mpctx->timeline + i; + mp_msg(MSGT_CPLAYER, MSGL_V, "%3d %9.3f %9.3f %p/%s\n", i, p->start, + p->source_start, p->source, p->source->filename); + } + mp_msg(MSGT_CPLAYER, MSGL_V, "END %9.3f\n", + mpctx->timeline[part_count].start); + } +} + +static void add_subtitle_fonts_from_sources(struct MPContext *mpctx) +{ +#ifdef CONFIG_ASS + if (mpctx->opts->ass_enabled) { + for (int j = 0; j < mpctx->num_sources; j++) { + struct demuxer *d = mpctx->sources[j]; + for (int i = 0; i < d->num_attachments; i++) { + struct demux_attachment *att = d->attachments + i; + if (mpctx->opts->use_embedded_fonts && attachment_is_font(att)) + ass_add_font(mpctx->ass_library, att->name, att->data, + att->data_size); + } + } + } + + // libass seems to misbehave if fonts are changed while a renderer + // exists, so we (re)create the renderer after fonts are set. + assert(!mpctx->osd->ass_renderer); + mpctx->osd->ass_renderer = ass_renderer_init(mpctx->osd->ass_library); + if (mpctx->osd->ass_renderer) + mp_ass_configure_fonts(mpctx->osd->ass_renderer, + mpctx->opts->sub_text_style); +#endif +} + +static struct mp_resolve_result *resolve_url(const char *filename, + struct MPOpts *opts) +{ +#if defined(CONFIG_LIBQUVI) || defined(CONFIG_LIBQUVI9) + return mp_resolve_quvi(filename, opts); +#else + return NULL; +#endif +} + +// Waiting for the slave master to send us a new file to play. +static void idle_loop(struct MPContext *mpctx) +{ + // ================= idle loop (STOP state) ========================= + while (mpctx->opts->player_idle_mode && !mpctx->playlist->current + && mpctx->stop_play != PT_QUIT) + { + uninit_player(mpctx, INITIALIZED_AO | INITIALIZED_VO); + mp_cmd_t *cmd; + while (!(cmd = mp_input_get_cmd(mpctx->input, + get_wakeup_period(mpctx) * 1000, + false))); + run_command(mpctx, cmd); + mp_cmd_free(cmd); + } +} + +static void stream_dump(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + char *filename = opts->stream_dump; + stream_t *stream = mpctx->stream; + assert(stream && filename); + + stream_set_capture_file(stream, filename); + + while (mpctx->stop_play == KEEP_PLAYING && !stream->eof) { + if (!opts->quiet && ((stream->pos / (1024 * 1024)) % 2) == 1) { + uint64_t pos = stream->pos - stream->start_pos; + uint64_t end = stream->end_pos - stream->start_pos; + char *line = talloc_asprintf(NULL, "Dumping %lld/%lld...", + (long long int)pos, (long long int)end); + write_status_line(mpctx, line); + talloc_free(line); + } + stream_fill_buffer(stream); + for (;;) { + mp_cmd_t *cmd = mp_input_get_cmd(mpctx->input, 0, false); + if (!cmd) + break; + run_command(mpctx, cmd); + talloc_free(cmd); + } + } +} + +// Start playing the current playlist entry. +// Handle initialization and deinitialization. +static void play_current_file(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + + mpctx->stop_play = 0; + mpctx->filename = NULL; + + if (mpctx->playlist->current) + mpctx->filename = mpctx->playlist->current->filename; + + if (!mpctx->filename) + goto terminate_playback; + +#ifdef CONFIG_ENCODING + encode_lavc_discontinuity(mpctx->encode_lavc_ctx); +#endif + + mpctx->add_osd_seek_info &= OSD_SEEK_INFO_EDITION; + + load_per_protocol_config(mpctx->mconfig, mpctx->filename); + load_per_extension_config(mpctx->mconfig, mpctx->filename); + load_per_file_config(mpctx->mconfig, mpctx->filename, opts->use_filedir_conf); + + if (opts->vo.video_driver_list) + load_per_output_config(mpctx->mconfig, PROFILE_CFG_VO, + opts->vo.video_driver_list[0].name); + if (opts->audio_driver_list) + load_per_output_config(mpctx->mconfig, PROFILE_CFG_AO, + opts->audio_driver_list[0].name); + + if (opts->position_resume) + load_playback_resume(mpctx->mconfig, mpctx->filename); + + load_per_file_options(mpctx->mconfig, mpctx->playlist->current->params, + mpctx->playlist->current->num_params); + + if (opts->reset_options) { + for (int n = 0; opts->reset_options[n]; n++) { + const char *opt = opts->reset_options[n]; + if (strcmp(opt, "all") == 0) { + m_config_backup_all_opts(mpctx->mconfig); + } else { + m_config_backup_opt(mpctx->mconfig, opt); + } + } + } + + // We must enable getch2 here to be able to interrupt network connection + // or cache filling + if (opts->consolecontrols && !opts->slave_mode) { + if (mpctx->initialized_flags & INITIALIZED_GETCH2) + mp_tmsg(MSGT_CPLAYER, MSGL_WARN, + "WARNING: getch2_init called twice!\n"); + else + getch2_enable(); // prepare stdin for hotkeys... + mpctx->initialized_flags |= INITIALIZED_GETCH2; + mp_msg(MSGT_CPLAYER, MSGL_DBG2, "\n[[[init getch2]]]\n"); + } + +#ifdef CONFIG_ASS + if (opts->ass_style_override) + ass_set_style_overrides(mpctx->ass_library, opts->ass_force_style_list); +#endif + + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, "Playing %s.\n", mpctx->filename); + + //============ Open & Sync STREAM --- fork cache2 ==================== + + assert(mpctx->stream == NULL); + assert(mpctx->demuxer == NULL); + assert(mpctx->sh_audio == NULL); + assert(mpctx->sh_video == NULL); + assert(mpctx->sh_sub == NULL); + + char *stream_filename = mpctx->filename; + mpctx->resolve_result = resolve_url(stream_filename, opts); + if (mpctx->resolve_result) { + if (mpctx->resolve_result->playlist) { + // Replace entry with playlist contents + playlist_transfer_entries(mpctx->playlist, + mpctx->resolve_result->playlist); + if (mpctx->playlist->current) + playlist_remove(mpctx->playlist, mpctx->playlist->current); + goto terminate_playback; + } + stream_filename = mpctx->resolve_result->url; + } + mpctx->stream = stream_open(stream_filename, opts); + if (!mpctx->stream) { // error... + demux_was_interrupted(mpctx); + goto terminate_playback; + } + mpctx->initialized_flags |= INITIALIZED_STREAM; + + mpctx->stream->start_pos += opts->seek_to_byte; + + if (opts->stream_dump && opts->stream_dump[0]) { + stream_dump(mpctx); + goto terminate_playback; + } + + // CACHE2: initial prefill: 20% later: 5% (should be set by -cacheopts) + int res = stream_enable_cache_percent(&mpctx->stream, + opts->stream_cache_size, + opts->stream_cache_def_size, + opts->stream_cache_min_percent, + opts->stream_cache_seek_min_percent); + if (res == 0) + if (demux_was_interrupted(mpctx)) + goto terminate_playback; + + stream_set_capture_file(mpctx->stream, opts->stream_capture); + +#ifdef CONFIG_DVBIN +goto_reopen_demuxer: ; +#endif + + //============ Open DEMUXERS --- DETECT file type ======================= + + mpctx->audio_delay = opts->audio_delay; + + mpctx->demuxer = demux_open(mpctx->stream, opts->demuxer_name, NULL, opts); + mpctx->master_demuxer = mpctx->demuxer; + + if (!mpctx->demuxer) { + mp_tmsg(MSGT_CPLAYER, MSGL_ERR, "Failed to recognize file format.\n"); + goto terminate_playback; + } + + if (mpctx->demuxer->matroska_data.ordered_chapters) + build_ordered_chapter_timeline(mpctx); + + if (mpctx->demuxer->type == DEMUXER_TYPE_EDL) + build_edl_timeline(mpctx); + + if (mpctx->demuxer->type == DEMUXER_TYPE_CUE) + build_cue_timeline(mpctx); + + print_timeline(mpctx); + + if (!mpctx->num_sources) { + MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, + mpctx->demuxer); + } + + if (mpctx->timeline) { + // With Matroska, the "master" file usually dictates track layout etc. + // On the contrary, the EDL and CUE demuxers are empty wrappers, as + // well as Matroska ordered chapter playlist-like files. + for (int n = 0; n < mpctx->num_timeline_parts; n++) { + if (mpctx->timeline[n].source == mpctx->demuxer) + goto main_is_ok; + } + mpctx->demuxer = mpctx->timeline[0].source; + main_is_ok: ; + } + add_dvd_tracks(mpctx); + add_demuxer_tracks(mpctx, mpctx->demuxer); + + mpctx->timeline_part = 0; + if (mpctx->timeline) + timeline_set_part(mpctx, mpctx->timeline_part, true); + + mpctx->initialized_flags |= INITIALIZED_DEMUXER; + + add_subtitle_fonts_from_sources(mpctx); + + open_subtitles_from_options(mpctx); + open_subtitles_from_resolve(mpctx); + open_audiofiles_from_options(mpctx); + + check_previous_track_selection(mpctx); + + mpctx->current_track[STREAM_VIDEO] = + select_track(mpctx, STREAM_VIDEO, mpctx->opts->video_id, NULL); + mpctx->current_track[STREAM_AUDIO] = + select_track(mpctx, STREAM_AUDIO, mpctx->opts->audio_id, + mpctx->opts->audio_lang); + mpctx->current_track[STREAM_SUB] = + select_track(mpctx, STREAM_SUB, mpctx->opts->sub_id, + mpctx->opts->sub_lang); + + demux_info_print(mpctx->master_demuxer); + print_file_properties(mpctx, mpctx->filename); + + preselect_demux_streams(mpctx); + +#ifdef CONFIG_ENCODING + if (mpctx->encode_lavc_ctx && mpctx->current_track[STREAM_VIDEO]) + encode_lavc_expect_stream(mpctx->encode_lavc_ctx, AVMEDIA_TYPE_VIDEO); + if (mpctx->encode_lavc_ctx && mpctx->current_track[STREAM_AUDIO]) + encode_lavc_expect_stream(mpctx->encode_lavc_ctx, AVMEDIA_TYPE_AUDIO); +#endif + + reinit_video_chain(mpctx); + reinit_audio_chain(mpctx); + reinit_subs(mpctx); + + //================ SETUP STREAMS ========================== + + if (opts->force_fps && mpctx->sh_video) { + mpctx->sh_video->fps = opts->force_fps; + mp_tmsg(MSGT_CPLAYER, MSGL_INFO, + "FPS forced to be %5.3f.\n", mpctx->sh_video->fps); + } + + //==================== START PLAYING ======================= + + if (!mpctx->sh_video && !mpctx->sh_audio) { + mp_tmsg(MSGT_CPLAYER, MSGL_FATAL, + "No video or audio streams selected.\n"); +#ifdef CONFIG_DVBIN + if (mpctx->stream->type == STREAMTYPE_DVB) { + int dir; + int v = mpctx->last_dvb_step; + if (v > 0) + dir = DVB_CHANNEL_HIGHER; + else + dir = DVB_CHANNEL_LOWER; + + if (dvb_step_channel(mpctx->stream, dir)) { + mpctx->stop_play = PT_NEXT_ENTRY; + mpctx->dvbin_reopen = 1; + } + } +#endif + goto terminate_playback; + } + + mp_tmsg(MSGT_CPLAYER, MSGL_V, "Starting playback...\n"); + + mpctx->drop_frame_cnt = 0; + mpctx->dropped_frames = 0; + mpctx->max_frames = opts->play_frames; + + if (mpctx->max_frames == 0) { + mpctx->stop_play = PT_NEXT_ENTRY; + goto terminate_playback; + } + + mpctx->time_frame = 0; + mpctx->drop_message_shown = 0; + mpctx->restart_playback = true; + mpctx->video_pts = 0; + mpctx->last_vo_pts = MP_NOPTS_VALUE; + mpctx->last_seek_pts = 0; + mpctx->playback_pts = MP_NOPTS_VALUE; + mpctx->hrseek_active = false; + mpctx->hrseek_framedrop = false; + mpctx->step_frames = 0; + mpctx->backstep_active = false; + mpctx->total_avsync_change = 0; + mpctx->last_chapter_seek = -2; + mpctx->playing_msg_shown = false; + mpctx->paused = false; + mpctx->paused_for_cache = false; + mpctx->seek = (struct seek_params){ 0 }; + + // If there's a timeline force an absolute seek to initialize state + double startpos = rel_time_to_abs(mpctx, opts->play_start, -1); + if (startpos != -1 || mpctx->timeline) { + queue_seek(mpctx, MPSEEK_ABSOLUTE, startpos, 0); + execute_queued_seek(mpctx); + } + if (startpos == -1 && mpctx->resolve_result && + mpctx->resolve_result->start_time > 0) + { + queue_seek(mpctx, MPSEEK_ABSOLUTE, mpctx->resolve_result->start_time, 0); + execute_queued_seek(mpctx); + } + if (opts->chapterrange[0] > 0) { + if (mp_seek_chapter(mpctx, opts->chapterrange[0] - 1)) + execute_queued_seek(mpctx); + } + + get_relative_time(mpctx); // reset current delta + + if (mpctx->opts->pause) + pause_player(mpctx); + + mpctx->error_playing = false; + while (!mpctx->stop_play) + run_playloop(mpctx); + + mp_msg(MSGT_GLOBAL, MSGL_V, "EOF code: %d \n", mpctx->stop_play); + +#ifdef CONFIG_DVBIN + if (mpctx->dvbin_reopen) { + mpctx->stop_play = 0; + uninit_player(mpctx, INITIALIZED_ALL - (INITIALIZED_STREAM | INITIALIZED_GETCH2 | (opts->fixed_vo ? INITIALIZED_VO : 0))); + mpctx->dvbin_reopen = 0; + goto goto_reopen_demuxer; + } +#endif + +terminate_playback: // don't jump here after ao/vo/getch initialization! + + if (opts->position_save_on_quit && mpctx->stop_play != PT_RESTART) + mp_write_watch_later_conf(mpctx); + + if (mpctx->step_frames) + opts->pause = 1; + + mp_msg(MSGT_CPLAYER, MSGL_INFO, "\n"); + + // time to uninit all, except global stuff: + int uninitialize_parts = INITIALIZED_ALL; + if (opts->fixed_vo) + uninitialize_parts -= INITIALIZED_VO; + if ((opts->gapless_audio && mpctx->stop_play == AT_END_OF_FILE) || + mpctx->encode_lavc_ctx) + uninitialize_parts -= INITIALIZED_AO; + uninit_player(mpctx, uninitialize_parts); + + // xxx handle this as INITIALIZED_CONFIG? + m_config_restore_backups(mpctx->mconfig); + + mpctx->filename = NULL; + talloc_free(mpctx->resolve_result); + mpctx->resolve_result = NULL; + +#ifdef CONFIG_ASS + if (mpctx->osd->ass_renderer) + ass_renderer_done(mpctx->osd->ass_renderer); + mpctx->osd->ass_renderer = NULL; + ass_clear_fonts(mpctx->ass_library); +#endif +} + +// Determine the next file to play. Note that if this function returns non-NULL, +// it can have side-effects and mutate mpctx. +struct playlist_entry *mp_next_file(struct MPContext *mpctx, int direction) +{ + struct playlist_entry *next = playlist_get_next(mpctx->playlist, direction); + if (!next && mpctx->opts->loop_times >= 0) { + if (direction > 0) { + next = mpctx->playlist->first; + if (next && mpctx->opts->loop_times > 0) { + mpctx->opts->loop_times--; + if (mpctx->opts->loop_times == 0) + mpctx->opts->loop_times = -1; + } + } else { + next = mpctx->playlist->last; + } + } + return next; +} + +// Play all entries on the playlist, starting from the current entry. +// Return if all done. +static void play_files(struct MPContext *mpctx) +{ + mpctx->quit_player_rc = EXIT_NONE; + for (;;) { + idle_loop(mpctx); + if (mpctx->stop_play == PT_QUIT) + break; + + mpctx->error_playing = true; + play_current_file(mpctx); + if (mpctx->error_playing) { + if (!mpctx->quit_player_rc) { + mpctx->quit_player_rc = EXIT_NOTPLAYED; + } else if (mpctx->quit_player_rc == EXIT_PLAYED) { + mpctx->quit_player_rc = EXIT_SOMENOTPLAYED; + } + } else if (mpctx->quit_player_rc == EXIT_NOTPLAYED) { + mpctx->quit_player_rc = EXIT_SOMENOTPLAYED; + } else { + mpctx->quit_player_rc = EXIT_PLAYED; + } + if (mpctx->stop_play == PT_QUIT) + break; + + if (!mpctx->stop_play || mpctx->stop_play == AT_END_OF_FILE) + mpctx->stop_play = PT_NEXT_ENTRY; + + struct playlist_entry *new_entry = NULL; + + if (mpctx->stop_play == PT_NEXT_ENTRY) { + new_entry = mp_next_file(mpctx, +1); + } else if (mpctx->stop_play == PT_CURRENT_ENTRY) { + new_entry = mpctx->playlist->current; + } else if (mpctx->stop_play == PT_RESTART) { + // The same as PT_CURRENT_ENTRY, unless we decide that the current + // playlist entry can be removed during playback. + new_entry = mpctx->playlist->current; + } else { // PT_STOP + playlist_clear(mpctx->playlist); + } + + mpctx->playlist->current = new_entry; + mpctx->playlist->current_was_replaced = false; + mpctx->stop_play = 0; + + if (!mpctx->playlist->current && !mpctx->opts->player_idle_mode) + break; + } +} + +// Abort current playback and set the given entry to play next. +// e must be on the mpctx->playlist. +void mp_set_playlist_entry(struct MPContext *mpctx, struct playlist_entry *e) +{ + assert(playlist_entry_to_index(mpctx->playlist, e) >= 0); + mpctx->playlist->current = e; + mpctx->playlist->current_was_replaced = false; + mpctx->stop_play = PT_CURRENT_ENTRY; +} + +void mp_print_version(int always) +{ + mp_msg(MSGT_CPLAYER, always ? MSGL_INFO : MSGL_V, + "%s (C) 2000-2013 mpv/MPlayer/mplayer2 projects\n built on %s\n", mplayer_version, mplayer_builddate); +} + +static bool handle_help_options(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + int opt_exit = 0; + if (opts->audio_decoders && strcmp(opts->audio_decoders, "help") == 0) { + struct mp_decoder_list *list = mp_audio_decoder_list(); + mp_print_decoders(MSGT_CPLAYER, MSGL_INFO, "Audio decoders:", list); + talloc_free(list); + opt_exit = 1; + } + if (opts->video_decoders && strcmp(opts->video_decoders, "help") == 0) { + struct mp_decoder_list *list = mp_video_decoder_list(); + mp_print_decoders(MSGT_CPLAYER, MSGL_INFO, "Video decoders:", list); + talloc_free(list); + opt_exit = 1; + } +#ifdef CONFIG_X11 + if (opts->vo.fstype_list && strcmp(opts->vo.fstype_list[0], "help") == 0) { + fstype_help(); + mp_msg(MSGT_FIXME, MSGL_FIXME, "\n"); + opt_exit = 1; + } +#endif + if ((opts->demuxer_name && strcmp(opts->demuxer_name, "help") == 0) || + (opts->audio_demuxer_name && strcmp(opts->audio_demuxer_name, "help") == 0) || + (opts->sub_demuxer_name && strcmp(opts->sub_demuxer_name, "help") == 0)) { + demuxer_help(); + mp_msg(MSGT_CPLAYER, MSGL_INFO, "\n"); + opt_exit = 1; + } + if (opts->list_properties) { + property_print_help(); + opt_exit = 1; + } +#ifdef CONFIG_ENCODING + if (encode_lavc_showhelp(mpctx->opts)) + opt_exit = 1; +#endif + return opt_exit; +} + +#ifdef PTW32_STATIC_LIB +static void detach_ptw32(void) +{ + pthread_win32_thread_detach_np(); + pthread_win32_process_detach_np(); +} +#endif + +static void osdep_preinit(int *p_argc, char ***p_argv) +{ + char *enable_talloc = getenv("MPV_LEAK_REPORT"); + if (*p_argc > 1 && (strcmp((*p_argv)[1], "-leak-report") == 0 || + strcmp((*p_argv)[1], "--leak-report") == 0)) + enable_talloc = "1"; + if (enable_talloc && strcmp(enable_talloc, "1") == 0) + talloc_enable_leak_report(); + +#ifdef __MINGW32__ + mp_get_converted_argv(p_argc, p_argv); +#endif + +#ifdef PTW32_STATIC_LIB + pthread_win32_process_attach_np(); + pthread_win32_thread_attach_np(); + atexit(detach_ptw32); +#endif + +#if defined(__MINGW32__) || defined(__CYGWIN__) + // stop Windows from showing all kinds of annoying error dialogs + SetErrorMode(0x8003); +#endif + + load_termcap(NULL); // load key-codes + + mp_time_init(); +} + +/* This preprocessor directive is a hack to generate a mplayer-nomain.o object + * file for some tools to link against. */ +#ifndef DISABLE_MAIN +static int mpv_main(int argc, char *argv[]) +{ + osdep_preinit(&argc, &argv); + + if (argc >= 1) { + argc--; + argv++; + } + + struct MPContext *mpctx = talloc(NULL, MPContext); + *mpctx = (struct MPContext){ + .last_dvb_step = 1, + .terminal_osd_text = talloc_strdup(mpctx, ""), + .playlist = talloc_struct(mpctx, struct playlist, {0}), + }; + + // Create the config context and register the options + mpctx->mconfig = m_config_new(mpctx, sizeof(struct MPOpts), + &mp_default_opts, mp_opts, NULL); + mpctx->opts = mpctx->mconfig->optstruct; + mpctx->mconfig->includefunc = cfg_include; + mpctx->mconfig->use_profiles = true; + + struct MPOpts *opts = mpctx->opts; + + + mpctx->global = talloc_zero(mpctx, struct mpv_global); + mpctx->global->opts = opts; + + // Nothing must call mp_msg() before this + mp_msg_init(mpctx->global); + mpctx->log = mp_log_new(mpctx, mpctx->global->log, "!mpv"); + + init_libav(); + GetCpuCaps(&gCpuCaps); + screenshot_init(mpctx); + + // Preparse the command line + m_config_preparse_command_line(mpctx->mconfig, argc, argv); + + mp_print_version(false); + print_libav_versions(); + + if (!parse_cfgfiles(mpctx, mpctx->mconfig)) + exit_player(mpctx, EXIT_ERROR); + + int r = m_config_parse_mp_command_line(mpctx->mconfig, mpctx->playlist, + argc, argv); + if (r < 0) { + if (r <= M_OPT_EXIT) { + exit_player(mpctx, EXIT_NONE); + } else { + exit_player(mpctx, EXIT_ERROR); + } + } + + if (handle_help_options(mpctx)) + exit_player(mpctx, EXIT_NONE); + + mp_msg(MSGT_CPLAYER, MSGL_V, "Configuration: " CONFIGURATION "\n"); + mp_tmsg(MSGT_CPLAYER, MSGL_V, "Command line:"); + for (int i = 0; i < argc; i++) + mp_msg(MSGT_CPLAYER, MSGL_V, " '%s'", argv[i]); + mp_msg(MSGT_CPLAYER, MSGL_V, "\n"); + + if (!mpctx->playlist->first && !opts->player_idle_mode) { + mp_print_version(true); + mp_msg(MSGT_CPLAYER, MSGL_INFO, "%s", mp_gtext(mp_help_text)); + exit_player(mpctx, EXIT_NONE); + } + +#ifdef CONFIG_PRIORITY + set_priority(); +#endif + + init_input(mpctx); + +#ifdef CONFIG_ENCODING + if (opts->encode_output.file && *opts->encode_output.file) { + mpctx->encode_lavc_ctx = encode_lavc_init(&opts->encode_output); + if(!mpctx->encode_lavc_ctx) { + mp_msg(MSGT_VO, MSGL_INFO, "Encoding initialization failed."); + exit_player(mpctx, EXIT_ERROR); + } + m_config_set_option0(mpctx->mconfig, "vo", "lavc"); + m_config_set_option0(mpctx->mconfig, "ao", "lavc"); + m_config_set_option0(mpctx->mconfig, "fixed-vo", "yes"); + m_config_set_option0(mpctx->mconfig, "gapless-audio", "yes"); + mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE); + } +#endif + +#ifdef CONFIG_ASS + mpctx->ass_library = mp_ass_init(opts); +#endif + + mpctx->osd = osd_create(opts, mpctx->ass_library); + + mpctx->playlist->current = mpctx->playlist->first; + + play_files(mpctx); + + exit_player(mpctx, mpctx->stop_play == PT_QUIT ? EXIT_QUIT : mpctx->quit_player_rc); + + return 1; +} + +int main(int argc, char *argv[]) +{ +#ifdef CONFIG_COCOA + return cocoa_main(mpv_main, argc, argv); +#else + return mpv_main(argc, argv); +#endif +} + +#endif /* DISABLE_MAIN */ diff --git a/mpvcore/mpv_global.h b/mpvcore/mpv_global.h new file mode 100644 index 0000000000..546c585294 --- /dev/null +++ b/mpvcore/mpv_global.h @@ -0,0 +1,12 @@ +#ifndef MPV_MPV_H +#define MPV_MPV_H + +// This should be accessed by glue code only, never normal code. +// The only purpose of this is to make mpv library-safe. +// Think hard before adding new members. +struct mpv_global { + struct MPOpts *opts; + struct mp_log *log; +}; + +#endif diff --git a/mpvcore/options.c b/mpvcore/options.c new file mode 100644 index 0000000000..dcab2dc564 --- /dev/null +++ b/mpvcore/options.c @@ -0,0 +1,838 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_CFG_MPLAYER_H +#define MPLAYER_CFG_MPLAYER_H + +/* + * config for cfgparser + */ + +#include <stddef.h> +#include <sys/types.h> +#include <limits.h> + +#include "core/options.h" +#include "config.h" +#include "core/m_config.h" +#include "core/m_option.h" +#include "stream/tv.h" +#include "stream/stream_radio.h" +#include "video/csputils.h" +#include "sub/sub.h" +#include "audio/mixer.h" +#include "audio/filter/af.h" +#include "audio/decode/dec_audio.h" +#include "mp_core.h" +#include "osdep/priority.h" + +int network_bandwidth=0; +int network_cookies_enabled = 0; +char *network_useragent="MPlayer 1.1-4.7"; +char *network_referrer=NULL; +char **network_http_header_fields=NULL; + +extern char *lirc_configfile; + +extern int mp_msg_color; +extern int mp_msg_module; + +extern int dvd_speed; /* stream/stream_dvd.c */ + +/* defined in demux: */ +extern const m_option_t demux_rawaudio_opts[]; +extern const m_option_t demux_rawvideo_opts[]; +extern const m_option_t cdda_opts[]; + +extern int sws_flags; +extern const char pp_help[]; + +extern const char mp_help_text[]; + +static int print_version_opt(const m_option_t *opt, const char *name, + const char *param) +{ + mp_print_version(true); + exit(0); +} + +#ifdef CONFIG_RADIO +static const m_option_t radioopts_conf[]={ + {"device", &stream_radio_defaults.device, CONF_TYPE_STRING, 0, 0 ,0, NULL}, + {"driver", &stream_radio_defaults.driver, CONF_TYPE_STRING, 0, 0 ,0, NULL}, + {"channels", &stream_radio_defaults.channels, CONF_TYPE_STRING_LIST, 0, 0 ,0, NULL}, + {"volume", &stream_radio_defaults.volume, CONF_TYPE_INT, CONF_RANGE, 0 ,100, NULL}, + {"adevice", &stream_radio_defaults.adevice, CONF_TYPE_STRING, 0, 0 ,0, NULL}, + {"arate", &stream_radio_defaults.arate, CONF_TYPE_INT, CONF_MIN, 0 ,0, NULL}, + {"achannels", &stream_radio_defaults.achannels, CONF_TYPE_INT, CONF_MIN, 0 ,0, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} +}; +#endif /* CONFIG_RADIO */ + +#ifdef CONFIG_TV +static const m_option_t tvopts_conf[]={ + {"immediatemode", &stream_tv_defaults.immediate, CONF_TYPE_INT, CONF_RANGE, 0, 1, NULL}, + {"audio", &stream_tv_defaults.noaudio, CONF_TYPE_FLAG, 0, 1, 0, NULL}, + {"audiorate", &stream_tv_defaults.audiorate, CONF_TYPE_INT, 0, 0, 0, NULL}, + {"driver", &stream_tv_defaults.driver, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"device", &stream_tv_defaults.device, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"freq", &stream_tv_defaults.freq, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"channel", &stream_tv_defaults.channel, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"chanlist", &stream_tv_defaults.chanlist, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"norm", &stream_tv_defaults.norm, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"automute", &stream_tv_defaults.automute, CONF_TYPE_INT, CONF_RANGE, 0, 255, NULL}, +#if defined(CONFIG_TV_V4L2) + {"normid", &stream_tv_defaults.normid, CONF_TYPE_INT, 0, 0, 0, NULL}, +#endif + {"width", &stream_tv_defaults.width, CONF_TYPE_INT, 0, 0, 4096, NULL}, + {"height", &stream_tv_defaults.height, CONF_TYPE_INT, 0, 0, 4096, NULL}, + {"input", &stream_tv_defaults.input, CONF_TYPE_INT, 0, 0, 20, NULL}, + {"outfmt", &stream_tv_defaults.outfmt, CONF_TYPE_FOURCC, 0, 0, 0, NULL}, + {"fps", &stream_tv_defaults.fps, CONF_TYPE_FLOAT, 0, 0, 100.0, NULL}, + {"channels", &stream_tv_defaults.channels, CONF_TYPE_STRING_LIST, 0, 0, 0, NULL}, + {"brightness", &stream_tv_defaults.brightness, CONF_TYPE_INT, CONF_RANGE, -100, 100, NULL}, + {"contrast", &stream_tv_defaults.contrast, CONF_TYPE_INT, CONF_RANGE, -100, 100, NULL}, + {"hue", &stream_tv_defaults.hue, CONF_TYPE_INT, CONF_RANGE, -100, 100, NULL}, + {"saturation", &stream_tv_defaults.saturation, CONF_TYPE_INT, CONF_RANGE, -100, 100, NULL}, + {"gain", &stream_tv_defaults.gain, CONF_TYPE_INT, CONF_RANGE, -1, 100, NULL}, +#if defined(CONFIG_TV_V4L2) + {"amode", &stream_tv_defaults.amode, CONF_TYPE_INT, CONF_RANGE, 0, 3, NULL}, + {"volume", &stream_tv_defaults.volume, CONF_TYPE_INT, CONF_RANGE, 0, 65535, NULL}, + {"bass", &stream_tv_defaults.bass, CONF_TYPE_INT, CONF_RANGE, 0, 65535, NULL}, + {"treble", &stream_tv_defaults.treble, CONF_TYPE_INT, CONF_RANGE, 0, 65535, NULL}, + {"balance", &stream_tv_defaults.balance, CONF_TYPE_INT, CONF_RANGE, 0, 65535, NULL}, + {"forcechan", &stream_tv_defaults.forcechan, CONF_TYPE_INT, CONF_RANGE, 1, 2, NULL}, + {"forceaudio", &stream_tv_defaults.force_audio, CONF_TYPE_FLAG, 0, 0, 1, NULL}, + {"buffersize", &stream_tv_defaults.buffer_size, CONF_TYPE_INT, CONF_RANGE, 16, 1024, NULL}, + {"mjpeg", &stream_tv_defaults.mjpeg, CONF_TYPE_FLAG, 0, 0, 1, NULL}, + {"decimation", &stream_tv_defaults.decimation, CONF_TYPE_INT, CONF_RANGE, 1, 4, NULL}, + {"quality", &stream_tv_defaults.quality, CONF_TYPE_INT, CONF_RANGE, 0, 100, NULL}, +#ifdef CONFIG_ALSA + {"alsa", &stream_tv_defaults.alsa, CONF_TYPE_FLAG, 0, 0, 1, NULL}, +#endif /* CONFIG_ALSA */ +#endif /* defined(CONFIG_TV_V4L2) */ + {"adevice", &stream_tv_defaults.adevice, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"audioid", &stream_tv_defaults.audio_id, CONF_TYPE_INT, CONF_RANGE, 0, 9, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} +}; +#endif /* CONFIG_TV */ + +extern int pvr_param_aspect_ratio; +extern int pvr_param_sample_rate; +extern int pvr_param_audio_layer; +extern int pvr_param_audio_bitrate; +extern char *pvr_param_audio_mode; +extern int pvr_param_bitrate; +extern char *pvr_param_bitrate_mode; +extern int pvr_param_bitrate_peak; +extern char *pvr_param_stream_type; + +#ifdef CONFIG_PVR +static const m_option_t pvropts_conf[]={ + {"aspect", &pvr_param_aspect_ratio, CONF_TYPE_INT, 0, 1, 4, NULL}, + {"arate", &pvr_param_sample_rate, CONF_TYPE_INT, 0, 32000, 48000, NULL}, + {"alayer", &pvr_param_audio_layer, CONF_TYPE_INT, 0, 1, 2, NULL}, + {"abitrate", &pvr_param_audio_bitrate, CONF_TYPE_INT, 0, 32, 448, NULL}, + {"amode", &pvr_param_audio_mode, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"vbitrate", &pvr_param_bitrate, CONF_TYPE_INT, 0, 0, 0, NULL}, + {"vmode", &pvr_param_bitrate_mode, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"vpeak", &pvr_param_bitrate_peak, CONF_TYPE_INT, 0, 0, 0, NULL}, + {"fmt", &pvr_param_stream_type, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} +}; +#endif /* CONFIG_PVR */ + +extern const m_option_t dvbin_opts_conf[]; +extern const m_option_t lavfdopts_conf[]; + +extern int sws_chr_vshift; +extern int sws_chr_hshift; +extern float sws_chr_gblur; +extern float sws_lum_gblur; +extern float sws_chr_sharpen; +extern float sws_lum_sharpen; + +static const m_option_t scaler_filter_conf[]={ + {"lgb", &sws_lum_gblur, CONF_TYPE_FLOAT, 0, 0, 100.0, NULL}, + {"cgb", &sws_chr_gblur, CONF_TYPE_FLOAT, 0, 0, 100.0, NULL}, + {"cvs", &sws_chr_vshift, CONF_TYPE_INT, 0, 0, 0, NULL}, + {"chs", &sws_chr_hshift, CONF_TYPE_INT, 0, 0, 0, NULL}, + {"ls", &sws_lum_sharpen, CONF_TYPE_FLOAT, 0, -100.0, 100.0, NULL}, + {"cs", &sws_chr_sharpen, CONF_TYPE_FLOAT, 0, -100.0, 100.0, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} +}; + +extern char *dvd_device, *cdrom_device; + +extern double mf_fps; +extern char * mf_type; +extern const struct m_obj_list vf_obj_list; +extern const struct m_obj_list af_obj_list; +extern const struct m_obj_list vo_obj_list; +extern const struct m_obj_list ao_obj_list; + +static const m_option_t mfopts_conf[]={ + {"fps", &mf_fps, CONF_TYPE_DOUBLE, 0, 0, 0, NULL}, + {"type", &mf_type, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} +}; + +extern int mp_msg_levels[MSGT_MAX]; +extern int mp_msg_level_all; + +static const m_option_t msgl_config[]={ + { "all", &mp_msg_level_all, CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL}, + + { "global", &mp_msg_levels[MSGT_GLOBAL], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "cplayer", &mp_msg_levels[MSGT_CPLAYER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "vo", &mp_msg_levels[MSGT_VO], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "ao", &mp_msg_levels[MSGT_AO], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "demuxer", &mp_msg_levels[MSGT_DEMUXER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "ds", &mp_msg_levels[MSGT_DS], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "demux", &mp_msg_levels[MSGT_DEMUX], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "header", &mp_msg_levels[MSGT_HEADER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "avsync", &mp_msg_levels[MSGT_AVSYNC], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "autoq", &mp_msg_levels[MSGT_AUTOQ], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "cfgparser", &mp_msg_levels[MSGT_CFGPARSER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "decaudio", &mp_msg_levels[MSGT_DECAUDIO], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "decvideo", &mp_msg_levels[MSGT_DECVIDEO], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "seek", &mp_msg_levels[MSGT_SEEK], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "win32", &mp_msg_levels[MSGT_WIN32], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "open", &mp_msg_levels[MSGT_OPEN], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "dvd", &mp_msg_levels[MSGT_DVD], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "parsees", &mp_msg_levels[MSGT_PARSEES], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "lirc", &mp_msg_levels[MSGT_LIRC], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "stream", &mp_msg_levels[MSGT_STREAM], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "cache", &mp_msg_levels[MSGT_CACHE], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "encode", &mp_msg_levels[MSGT_ENCODE], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "xacodec", &mp_msg_levels[MSGT_XACODEC], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "tv", &mp_msg_levels[MSGT_TV], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "radio", &mp_msg_levels[MSGT_RADIO], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "osdep", &mp_msg_levels[MSGT_OSDEP], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "spudec", &mp_msg_levels[MSGT_SPUDEC], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "playtree", &mp_msg_levels[MSGT_PLAYTREE], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "input", &mp_msg_levels[MSGT_INPUT], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "vfilter", &mp_msg_levels[MSGT_VFILTER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "osd", &mp_msg_levels[MSGT_OSD], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "network", &mp_msg_levels[MSGT_NETWORK], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "cpudetect", &mp_msg_levels[MSGT_CPUDETECT], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "codeccfg", &mp_msg_levels[MSGT_CODECCFG], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "sws", &mp_msg_levels[MSGT_SWS], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "vobsub", &mp_msg_levels[MSGT_VOBSUB], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "subreader", &mp_msg_levels[MSGT_SUBREADER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "afilter", &mp_msg_levels[MSGT_AFILTER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "netst", &mp_msg_levels[MSGT_NETST], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "muxer", &mp_msg_levels[MSGT_MUXER], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "identify", &mp_msg_levels[MSGT_IDENTIFY], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "ass", &mp_msg_levels[MSGT_ASS], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "statusline", &mp_msg_levels[MSGT_STATUSLINE], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + { "fixme", &mp_msg_levels[MSGT_FIXME], CONF_TYPE_INT, CONF_RANGE, -1, 9, NULL }, + {"help", "Available msg modules:\n" + " global - common player errors/information\n" + " cplayer - console player (mplayer.c)\n" + " vo - libvo\n" + " ao - libao\n" + " demuxer - demuxer.c (general stuff)\n" + " ds - demux stream (add/read packet etc)\n" + " demux - fileformat-specific stuff (demux_*.c)\n" + " header - fileformat-specific header (*header.c)\n" + " avsync - mplayer.c timer stuff\n" + " autoq - mplayer.c auto-quality stuff\n" + " cfgparser - cfgparser.c\n" + " decaudio - av decoder\n" + " decvideo\n" + " seek - seeking code\n" + " win32 - win32 dll stuff\n" + " open - open.c (stream opening)\n" + " dvd - open.c (DVD init/read/seek)\n" + " parsees - parse_es.c (mpeg stream parser)\n" + " lirc - lirc_mp.c and input lirc driver\n" + " stream - stream.c\n" + " cache - cache2.c\n" + " encode - encode_lavc.c and associated vo/ao drivers\n" + " xacodec - XAnim codecs\n" + " tv - TV input subsystem\n" + " osdep - OS-dependent parts\n" + " spudec - spudec.c\n" + " playtree - Playtree handling (playtree.c, playtreeparser.c)\n" + " input\n" + " vfilter\n" + " osd\n" + " network\n" + " cpudetect\n" + " codeccfg\n" + " sws\n" + " vobsub\n" + " subreader\n" + " afilter - Audio filter messages\n" + " netst - Netstream\n" + " muxer - muxer layer\n" + " identify - identify output\n" + " ass - libass messages\n" + " statusline - playback/encoding status line\n" + " fixme - messages not yet fixed to map to module\n" + "\n", CONF_TYPE_PRINT, CONF_GLOBAL | CONF_NOCFG, 0, 0, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} + +}; + +#ifdef CONFIG_TV +static const m_option_t tvscan_conf[]={ + {"autostart", &stream_tv_defaults.scan, CONF_TYPE_FLAG, 0, 0, 1, NULL}, + {"threshold", &stream_tv_defaults.scan_threshold, CONF_TYPE_INT, CONF_RANGE, 1, 100, NULL}, + {"period", &stream_tv_defaults.scan_period, CONF_TYPE_FLOAT, CONF_RANGE, 0.1, 2.0, NULL}, + {NULL, NULL, 0, 0, 0, 0, NULL} +}; +#endif + +#define OPT_BASE_STRUCT struct MPOpts + +extern const struct m_sub_options image_writer_conf; + +static const m_option_t screenshot_conf[] = { + OPT_SUBSTRUCT("", screenshot_image_opts, image_writer_conf, 0), + OPT_STRING("template", screenshot_template, 0), + {0}, +}; + +extern const m_option_t lavc_decode_opts_conf[]; +extern const m_option_t ad_lavc_decode_opts_conf[]; + +extern const m_option_t mp_input_opts[]; + +const m_option_t mp_opts[] = { + // handled in command line pre-parser (parser-mpcmd.c) + {"v", NULL, CONF_TYPE_STORE, CONF_GLOBAL | CONF_NOCFG, 0, 0, NULL}, + + // handled in command line parser (parser-mpcmd.c) + {"playlist", NULL, CONF_TYPE_STRING, CONF_NOCFG | M_OPT_MIN, 1, 0, NULL}, + {"shuffle", NULL, CONF_TYPE_FLAG, CONF_NOCFG, 0, 0, NULL}, + {"{", NULL, CONF_TYPE_STORE, CONF_NOCFG, 0, 0, NULL}, + {"}", NULL, CONF_TYPE_STORE, CONF_NOCFG, 0, 0, NULL}, + + // handled in m_config.c + { "include", NULL, CONF_TYPE_STRING }, + { "profile", NULL, CONF_TYPE_STRING_LIST }, + { "show-profile", NULL, CONF_TYPE_STRING, CONF_NOCFG }, + { "list-options", NULL, CONF_TYPE_STORE, CONF_NOCFG }, + + // handled in mplayer.c (looks at the raw argv[]) + {"leak-report", "", CONF_TYPE_STORE, CONF_GLOBAL | CONF_NOCFG }, + +// ------------------------- common options -------------------- + OPT_FLAG("quiet", quiet, CONF_GLOBAL), + {"really-quiet", &verbose, CONF_TYPE_STORE, CONF_GLOBAL|CONF_PRE_PARSE, 0, -10, NULL}, + {"msglevel", (void *) msgl_config, CONF_TYPE_SUBCONFIG, CONF_GLOBAL, 0, 0, NULL}, + {"msgcolor", &mp_msg_color, CONF_TYPE_FLAG, CONF_GLOBAL | CONF_PRE_PARSE, 0, 1, NULL}, + {"msgmodule", &mp_msg_module, CONF_TYPE_FLAG, CONF_GLOBAL, 0, 1, NULL}, +#ifdef CONFIG_PRIORITY + {"priority", &proc_priority, CONF_TYPE_STRING, 0, 0, 0, NULL}, +#endif + OPT_FLAG("config", load_config, CONF_GLOBAL | CONF_NOCFG | CONF_PRE_PARSE), + OPT_STRINGLIST("reset-on-next-file", reset_options, CONF_GLOBAL), + +// ------------------------- stream options -------------------- + +#ifdef CONFIG_STREAM_CACHE + OPT_CHOICE_OR_INT("cache", stream_cache_size, 0, 32, 0x7fffffff, + ({"no", 0}, + {"auto", -1}), + OPTDEF_INT(-1)), + OPT_CHOICE_OR_INT("cache-default", stream_cache_def_size, 0, 32, 0x7fffffff, + ({"no", 0}), + OPTDEF_INT(320)), + OPT_FLOATRANGE("cache-min", stream_cache_min_percent, 0, 0, 99), + OPT_FLOATRANGE("cache-seek-min", stream_cache_seek_min_percent, 0, 0, 99), + OPT_CHOICE_OR_INT("cache-pause", stream_cache_pause, 0, + 0, 40, ({"no", -1})), +#endif /* CONFIG_STREAM_CACHE */ + {"cdrom-device", &cdrom_device, CONF_TYPE_STRING, 0, 0, 0, NULL}, +#ifdef CONFIG_DVDREAD + {"dvd-device", &dvd_device, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"dvd-speed", &dvd_speed, CONF_TYPE_INT, 0, 0, 0, NULL}, + {"dvdangle", &dvd_angle, CONF_TYPE_INT, CONF_RANGE, 1, 99, NULL}, +#endif /* CONFIG_DVDREAD */ + OPT_INTPAIR("chapter", chapterrange, 0), + OPT_INTRANGE("edition", edition_id, 0, -1, 8190), +#ifdef CONFIG_LIBBLURAY + {"bluray-device", &bluray_device, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"bluray-angle", &bluray_angle, CONF_TYPE_INT, CONF_RANGE, 0, 999, NULL}, +#endif /* CONFIG_LIBBLURAY */ + + {"http-header-fields", &network_http_header_fields, CONF_TYPE_STRING_LIST, 0, 0, 0, NULL}, + {"user-agent", &network_useragent, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"referrer", &network_referrer, CONF_TYPE_STRING, 0, 0, 0, NULL}, + {"cookies", &network_cookies_enabled, CONF_TYPE_FLAG, 0, 0, 1, NULL}, + {"cookies-file", &cookies_file, CONF_TYPE_STRING, 0, 0, 0, NULL}, + +// ------------------------- demuxer options -------------------- + + OPT_CHOICE_OR_INT("frames", play_frames, 0, 0, INT_MAX, + ({"all", -1})), + + // seek to byte/seconds position + OPT_INT64("sb", seek_to_byte, 0), + OPT_REL_TIME("start", play_start, 0), + OPT_REL_TIME("end", play_end, 0), + OPT_REL_TIME("length", play_length, 0), + + OPT_FLAG("pause", pause, 0), + OPT_FLAG("keep-open", keep_open, 0), + + // AVI and Ogg only: (re)build index at startup + OPT_FLAG_CONSTANTS("idx", index_mode, 0, -1, 1), + OPT_FLAG_STORE("forceidx", index_mode, 0, 2), + + // select audio/video/subtitle stream + OPT_TRACKCHOICE("aid", audio_id), + OPT_TRACKCHOICE("vid", video_id), + OPT_TRACKCHOICE("sid", sub_id), + OPT_FLAG_STORE("no-sub", sub_id, 0, -2), + OPT_FLAG_STORE("no-video", video_id, 0, -2), + OPT_FLAG_STORE("no-audio", audio_id, 0, -2), + OPT_STRINGLIST("alang", audio_lang, 0), + OPT_STRINGLIST("slang", sub_lang, 0), + + OPT_CHOICE("audio-display", audio_display, 0, + ({"no", 0}, {"attachment", 1})), + + OPT_STRING("quvi-format", quvi_format, 0), + +#ifdef CONFIG_CDDA + { "cdda", (void *)&cdda_opts, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, +#endif + + // demuxer.c - select audio/sub file/demuxer + OPT_STRING("audiofile", audio_stream, 0), + OPT_INTRANGE("audiofile-cache", audio_stream_cache, 0, 50, 65536), + OPT_STRING("demuxer", demuxer_name, 0), + OPT_STRING("audio-demuxer", audio_demuxer_name, 0), + OPT_STRING("sub-demuxer", sub_demuxer_name, 0), + + {"mf", (void *) mfopts_conf, CONF_TYPE_SUBCONFIG, 0,0,0, NULL}, +#ifdef CONFIG_RADIO + {"radio", (void *) radioopts_conf, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, +#endif /* CONFIG_RADIO */ +#ifdef CONFIG_TV + {"tv", (void *) tvopts_conf, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, +#endif /* CONFIG_TV */ +#ifdef CONFIG_PVR + {"pvr", (void *) pvropts_conf, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, +#endif /* CONFIG_PVR */ +#ifdef CONFIG_DVBIN + {"dvbin", (void *) dvbin_opts_conf, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, +#endif + +// ------------------------- a-v sync options -------------------- + + // set A-V sync correction speed (0=disables it): + OPT_FLOATRANGE("mc", default_max_pts_correction, 0, 0, 100), + + // force video/audio rate: + OPT_DOUBLE("fps", force_fps, CONF_MIN, 0), + OPT_INTRANGE("srate", force_srate, 0, 1000, 8*48000), + OPT_CHMAP("channels", audio_output_channels, CONF_MIN, .min = 1), + OPT_AUDIOFORMAT("format", audio_output_format, 0), + OPT_DOUBLE("speed", playback_speed, M_OPT_RANGE, .min = 0.01, .max = 100.0), + + // set a-v distance + OPT_FLOATRANGE("audio-delay", audio_delay, 0, -100.0, 100.0), + +// ------------------------- codec/vfilter options -------------------- + + OPT_SETTINGSLIST("af*", af_settings, 0, &af_obj_list), + OPT_SETTINGSLIST("vf*", vf_settings, 0, &vf_obj_list), + + OPT_STRING("ad", audio_decoders, 0), + OPT_STRING("vd", video_decoders, 0), + + OPT_FLAG("ad-spdif-dtshd", dtshd, 0), + OPT_FLAG("dtshd", dtshd, 0), // old alias + + OPT_CHOICE("hwdec", hwdec_api, 0, + ({"no", 0}, + {"vdpau", 1}, + {"vda", 2}, + {"crystalhd", 3})), + OPT_STRING("hwdec-codecs", hwdec_codecs, 0), + + // postprocessing: + OPT_INT("pp", divx_quality, 0), +#ifdef CONFIG_LIBPOSTPROC + {"pphelp", (void *) &pp_help, CONF_TYPE_PRINT, CONF_GLOBAL | CONF_NOCFG, 0, 0, NULL}, +#endif + + // scaling: + {"sws", &sws_flags, CONF_TYPE_INT, 0, 0, 2, NULL}, + {"ssf", (void *) scaler_filter_conf, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, + OPT_FLOATRANGE("aspect", movie_aspect, 0, 0.1, 10.0), + OPT_FLOAT_STORE("no-aspect", movie_aspect, 0, 0), + + OPT_FLAG_CONSTANTS("flip", flip, 0, 0, 1), + + OPT_CHOICE("field-dominance", field_dominance, 0, + ({"auto", -1}, {"top", 0}, {"bottom", 1})), + + {"vd-lavc", (void *) lavc_decode_opts_conf, CONF_TYPE_SUBCONFIG}, + {"ad-lavc", (void *) ad_lavc_decode_opts_conf, CONF_TYPE_SUBCONFIG}, + + {"demuxer-lavf", (void *) lavfdopts_conf, CONF_TYPE_SUBCONFIG}, + {"demuxer-rawaudio", (void *)&demux_rawaudio_opts, CONF_TYPE_SUBCONFIG}, + {"demuxer-rawvideo", (void *)&demux_rawvideo_opts, CONF_TYPE_SUBCONFIG}, + + OPT_FLAG("demuxer-mkv-subtitle-preroll", mkv_subtitle_preroll, 0), + OPT_FLAG("mkv-subtitle-preroll", mkv_subtitle_preroll, 0), // old alias + +// ------------------------- subtitles options -------------------- + + OPT_STRINGLIST("sub", sub_name, 0), + OPT_PATHLIST("sub-paths", sub_paths, 0), + OPT_STRING("subcp", sub_cp, 0), + OPT_FLOAT("sub-delay", sub_delay, 0), + OPT_FLOAT("subfps", sub_fps, 0), + OPT_FLOAT("sub-speed", sub_speed, 0), + OPT_FLAG("autosub", sub_auto, 0), + OPT_FLAG("sub-visibility", sub_visibility, 0), + OPT_FLAG("sub-forced-only", forced_subs_only, 0), + OPT_FLAG_CONSTANTS("sub-fix-timing", suboverlap_enabled, 0, 1, 0), + OPT_CHOICE("autosub-match", sub_match_fuzziness, 0, + ({"exact", 0}, {"fuzzy", 1}, {"all", 2})), + OPT_INTRANGE("sub-pos", sub_pos, 0, 0, 100), + OPT_FLOATRANGE("sub-gauss", sub_gauss, 0, 0.0, 3.0), + OPT_FLAG("sub-gray", sub_gray, 0), + OPT_FLAG("ass", ass_enabled, 0), + OPT_FLOATRANGE("sub-scale", sub_scale, 0, 0, 100), + OPT_FLOATRANGE("ass-line-spacing", ass_line_spacing, 0, -1000, 1000), + OPT_FLAG("ass-use-margins", ass_use_margins, 0), + OPT_FLAG("ass-vsfilter-aspect-compat", ass_vsfilter_aspect_compat, 0), + OPT_CHOICE("ass-vsfilter-color-compat", ass_vsfilter_color_compat, 0, + ({"no", 0}, {"basic", 1}, {"full", 2}, {"force-601", 3})), + OPT_FLAG("ass-vsfilter-blur-compat", ass_vsfilter_blur_compat, 0), + OPT_FLAG("embeddedfonts", use_embedded_fonts, 0), + OPT_STRINGLIST("ass-force-style", ass_force_style_list, 0), + OPT_STRING("ass-styles", ass_styles_file, 0), + OPT_INTRANGE("ass-hinting", ass_hinting, 0, 0, 7), + OPT_CHOICE("ass-style-override", ass_style_override, 0, + ({"no", 0}, {"yes", 1})), + OPT_FLAG("osd-bar", osd_bar_visible, 0), + OPT_FLOATRANGE("osd-bar-align-x", osd_bar_align_x, 0, -1.0, +1.0), + OPT_FLOATRANGE("osd-bar-align-y", osd_bar_align_y, 0, -1.0, +1.0), + OPT_FLOATRANGE("osd-bar-w", osd_bar_w, 0, 1, 100), + OPT_FLOATRANGE("osd-bar-h", osd_bar_h, 0, 0.1, 50), + OPT_SUBSTRUCT("osd", osd_style, osd_style_conf, 0), + OPT_SUBSTRUCT("sub-text", sub_text_style, osd_style_conf, 0), + +//---------------------- libao/libvo options ------------------------ + OPT_SETTINGSLIST("vo", vo.video_driver_list, 0, &vo_obj_list), + OPT_SETTINGSLIST("ao", audio_driver_list, 0, &ao_obj_list), + OPT_FLAG("fixed-vo", fixed_vo, CONF_GLOBAL), + OPT_FLAG("ontop", vo.ontop, 0), + OPT_FLAG("border", vo.border, 0), + + OPT_CHOICE("softvol", softvol, 0, + ({"no", SOFTVOL_NO}, + {"yes", SOFTVOL_YES}, + {"auto", SOFTVOL_AUTO})), + OPT_FLOATRANGE("softvol-max", softvol_max, 0, 10, 10000), + OPT_INTRANGE("volstep", volstep, 0, 0, 100), + OPT_FLOATRANGE("volume", mixer_init_volume, 0, -1, 10000), + OPT_CHOICE("mute", mixer_init_mute, M_OPT_OPTIONAL_PARAM, + ({"auto", -1}, + {"no", 0}, + {"yes", 1}, {"", 1})), + OPT_FLAG("gapless-audio", gapless_audio, 0), + + // set screen dimensions (when not detectable or virtual!=visible) + OPT_INTRANGE("screenw", vo.screenwidth, CONF_GLOBAL, 0, 4096), + OPT_INTRANGE("screenh", vo.screenheight, CONF_GLOBAL, 0, 4096), + OPT_GEOMETRY("geometry", vo.geometry, 0), + OPT_SIZE_BOX("autofit", vo.autofit, 0), + OPT_SIZE_BOX("autofit-larger", vo.autofit_larger, 0), + OPT_FLAG("force-window-position", vo.force_window_position, 0), + // vo name (X classname) and window title strings + OPT_STRING("name", vo.winname, 0), + OPT_STRING("title", wintitle, 0), + // set aspect ratio of monitor - useful for 16:9 TV-out + OPT_FLOATRANGE("monitoraspect", vo.force_monitor_aspect, 0, 0.0, 9.0), + OPT_FLOATRANGE("monitorpixelaspect", vo.monitor_pixel_aspect, 0, 0.2, 9.0), + // start in fullscreen mode: + OPT_FLAG("fullscreen", vo.fullscreen, 0), + OPT_FLAG("fs", vo.fullscreen, 0), + // set fullscreen switch method (workaround for buggy WMs) + OPT_INTRANGE("fsmode-dontuse", vo.fsmode, 0, 31, 4096), + OPT_FLAG("native-keyrepeat", vo.native_keyrepeat, 0), + OPT_FLOATRANGE("panscan", vo.panscan, 0, 0.0, 1.0), + OPT_FLOATRANGE("panscanrange", vo.panscanrange, 0, -19.0, 99.0), + OPT_FLAG("force-rgba-osd-rendering", force_rgba_osd, 0), + OPT_CHOICE("colormatrix", requested_colorspace, 0, + ({"auto", MP_CSP_AUTO}, + {"BT.601", MP_CSP_BT_601}, + {"BT.709", MP_CSP_BT_709}, + {"SMPTE-240M", MP_CSP_SMPTE_240M}, + {"YCgCo", MP_CSP_YCGCO})), + OPT_CHOICE("colormatrix-input-range", requested_input_range, 0, + ({"auto", MP_CSP_LEVELS_AUTO}, + {"limited", MP_CSP_LEVELS_TV}, + {"full", MP_CSP_LEVELS_PC})), + OPT_CHOICE("colormatrix-output-range", requested_output_range, 0, + ({"auto", MP_CSP_LEVELS_AUTO}, + {"limited", MP_CSP_LEVELS_TV}, + {"full", MP_CSP_LEVELS_PC})), + + OPT_CHOICE_OR_INT("cursor-autohide", vo.cursor_autohide_delay, 0, + 0, 30000, ({"no", -1}, {"always", -2})), + OPT_FLAG("stop-screensaver", stop_screensaver, 0), + + OPT_INT64("wid", vo.WinID, CONF_GLOBAL), +#ifdef CONFIG_X11 + OPT_STRINGLIST("fstype", vo.fstype_list, 0), +#endif + OPT_STRING("heartbeat-cmd", heartbeat_cmd, 0), + OPT_FLOAT("heartbeat-interval", heartbeat_interval, CONF_MIN, 0), + OPT_FLAG_CONSTANTS("mouseinput", vo.nomouse_input, 0, 1, 0), + + OPT_CHOICE_OR_INT("screen", vo.screen_id, 0, 0, 32, + ({"default", -1})), + + OPT_CHOICE_OR_INT("fs-screen", vo.fsscreen_id, 0, 0, 32, + ({"all", -2}, {"current", -1})), + +#ifdef CONFIG_COCOA + OPT_FLAG("native-fs", vo.native_fs, 0), +#endif + + OPT_INTRANGE("brightness", gamma_brightness, 0, -100, 100), + OPT_INTRANGE("saturation", gamma_saturation, 0, -100, 100), + OPT_INTRANGE("contrast", gamma_contrast, 0, -100, 100), + OPT_INTRANGE("hue", gamma_hue, 0, -100, 100), + OPT_INTRANGE("gamma", gamma_gamma, 0, -100, 100), + OPT_FLAG("keepaspect", vo.keepaspect, 0), + +//---------------------- mplayer-only options ------------------------ + + OPT_FLAG("use-filedir-conf", use_filedir_conf, CONF_GLOBAL), + OPT_CHOICE("osd-level", osd_level, 0, + ({"0", 0}, {"1", 1}, {"2", 2}, {"3", 3})), + OPT_INTRANGE("osd-duration", osd_duration, 0, 0, 3600000), + OPT_FLAG("osd-fractions", osd_fractions, 0), + OPT_FLOATRANGE("osd-scale", osd_scale, 0, 0, 100), + + OPT_DOUBLE("sstep", step_sec, CONF_MIN, 0), + + OPT_CHOICE("framedrop", frame_dropping, 0, + ({"no", 0}, + {"yes", 1}, + {"hard", 2})), + + OPT_FLAG("untimed", untimed, 0), + + OPT_STRING("stream-capture", stream_capture, 0), + OPT_STRING("stream-dump", stream_dump, 0), + +#ifdef CONFIG_LIRC + {"lircconf", &lirc_configfile, CONF_TYPE_STRING, CONF_GLOBAL, 0, 0, NULL}, +#endif + + OPT_CHOICE_OR_INT("loop", loop_times, M_OPT_GLOBAL, 1, 10000, + ({"no", -1}, {"0", -1}, + {"inf", 0})), + + OPT_FLAG("resume-playback", position_resume, 0), + OPT_FLAG("save-position-on-quit", position_save_on_quit, 0), + + OPT_FLAG("ordered-chapters", ordered_chapters, 0), + OPT_INTRANGE("chapter-merge-threshold", chapter_merge_threshold, 0, 0, 10000), + + // a-v sync stuff: + OPT_FLAG("correct-pts", correct_pts, 0), + OPT_CHOICE("pts-association-mode", user_pts_assoc_mode, 0, + ({"auto", 0}, {"decoder", 1}, {"sort", 2})), + OPT_FLAG("initial-audio-sync", initial_audio_sync, 0), + OPT_CHOICE("hr-seek", hr_seek, 0, + ({"no", -1}, {"absolute", 0}, {"always", 1}, {"yes", 1})), + OPT_FLOATRANGE("hr-seek-demuxer-offset", hr_seek_demuxer_offset, 0, -9, 99), + OPT_CHOICE_OR_INT("autosync", autosync, 0, 0, 10000, + ({"no", -1})), + + OPT_FLAG("softsleep", softsleep, 0), + + OPT_CHOICE("term-osd", term_osd, 0, + ({"force", 1}, + {"auto", 2}, + {"no", 0})), + + OPT_STRING("term-osd-esc", term_osd_esc, M_OPT_PARSE_ESCAPES, + OPTDEF_STR("\x1b[A\r\x1b[K")), + OPT_STRING("playing-msg", playing_msg, M_OPT_PARSE_ESCAPES), + OPT_STRING("status-msg", status_msg, M_OPT_PARSE_ESCAPES), + OPT_STRING("osd-status-msg", osd_status_msg, M_OPT_PARSE_ESCAPES), + + OPT_FLAG("slave-broken", slave_mode, CONF_GLOBAL), + OPT_FLAG("idle", player_idle_mode, CONF_GLOBAL), + OPT_INTRANGE("key-fifo-size", input.key_fifo_size, CONF_GLOBAL, 2, 65000), + OPT_FLAG("consolecontrols", consolecontrols, CONF_GLOBAL), + OPT_FLAG("mouse-movements", vo.enable_mouse_movements, CONF_GLOBAL), +#ifdef CONFIG_TV + {"tvscan", (void *) tvscan_conf, CONF_TYPE_SUBCONFIG, 0, 0, 0, NULL}, +#endif /* CONFIG_TV */ + + {"screenshot", (void *) screenshot_conf, CONF_TYPE_SUBCONFIG}, + + {"", (void *) mp_input_opts, CONF_TYPE_SUBCONFIG}, + + OPT_FLAG("list-properties", list_properties, CONF_GLOBAL), + {"identify", &mp_msg_levels[MSGT_IDENTIFY], CONF_TYPE_FLAG, CONF_GLOBAL, 0, MSGL_V, NULL}, + {"help", (void *) mp_help_text, CONF_TYPE_PRINT, CONF_NOCFG|CONF_GLOBAL, 0, 0, NULL}, + {"h", (void *) mp_help_text, CONF_TYPE_PRINT, CONF_NOCFG|CONF_GLOBAL, 0, 0, NULL}, + {"version", (void *)print_version_opt, CONF_TYPE_PRINT_FUNC, CONF_NOCFG|CONF_GLOBAL|M_OPT_PRE_PARSE}, + {"V", (void *)print_version_opt, CONF_TYPE_PRINT_FUNC, CONF_NOCFG|CONF_GLOBAL|M_OPT_PRE_PARSE}, + +#ifdef CONFIG_ENCODING + OPT_STRING("o", encode_output.file, CONF_GLOBAL), + OPT_STRING("of", encode_output.format, CONF_GLOBAL), + OPT_STRINGLIST("ofopts*", encode_output.fopts, CONF_GLOBAL), + OPT_FLOATRANGE("ofps", encode_output.fps, CONF_GLOBAL, 0.0, 1000000.0), + OPT_FLOATRANGE("omaxfps", encode_output.maxfps, CONF_GLOBAL, 0.0, 1000000.0), + OPT_STRING("ovc", encode_output.vcodec, CONF_GLOBAL), + OPT_STRINGLIST("ovcopts*", encode_output.vopts, CONF_GLOBAL), + OPT_STRING("oac", encode_output.acodec, CONF_GLOBAL), + OPT_STRINGLIST("oacopts*", encode_output.aopts, CONF_GLOBAL), + OPT_FLAG("oharddup", encode_output.harddup, CONF_GLOBAL), + OPT_FLOATRANGE("ovoffset", encode_output.voffset, CONF_GLOBAL, -1000000.0, 1000000.0), + OPT_FLOATRANGE("oaoffset", encode_output.aoffset, CONF_GLOBAL, -1000000.0, 1000000.0), + OPT_FLAG("ocopyts", encode_output.copyts, CONF_GLOBAL), + OPT_FLAG("orawts", encode_output.rawts, CONF_GLOBAL), + OPT_FLAG("oautofps", encode_output.autofps, CONF_GLOBAL), + OPT_FLAG("oneverdrop", encode_output.neverdrop, CONF_GLOBAL), + OPT_FLAG("ovfirst", encode_output.video_first, CONF_GLOBAL), + OPT_FLAG("oafirst", encode_output.audio_first, CONF_GLOBAL), +#endif + + {NULL, NULL, 0, 0, 0, 0, NULL} +}; + +const struct MPOpts mp_default_opts = { + .reset_options = (char **)(const char *[]){"pause", NULL}, + .audio_driver_list = NULL, + .audio_decoders = "-spdif:*", // never select spdif by default + .video_decoders = NULL, + .fixed_vo = 1, + .softvol = SOFTVOL_AUTO, + .softvol_max = 200, + .mixer_init_volume = -1, + .mixer_init_mute = -1, + .volstep = 3, + .vo = { + .video_driver_list = NULL, + .cursor_autohide_delay = 1000, + .monitor_pixel_aspect = 1.0, + .panscanrange = 1.0, + .screen_id = -1, + .fsscreen_id = -1, + .nomouse_input = 0, + .enable_mouse_movements = 1, + .fsmode = 0, + .panscan = 0.0f, + .keepaspect = 1, + .border = 1, + .WinID = -1, + }, + .wintitle = "mpv - ${media-title}", + .heartbeat_interval = 30.0, + .stop_screensaver = 1, + .gamma_gamma = 1000, + .gamma_brightness = 1000, + .gamma_contrast = 1000, + .gamma_saturation = 1000, + .gamma_hue = 1000, + .osd_level = 1, + .osd_duration = 1000, + .osd_bar_align_y = 0.5, + .osd_bar_w = 75.0, + .osd_bar_h = 3.125, + .osd_scale = 1, + .loop_times = -1, + .ordered_chapters = 1, + .chapter_merge_threshold = 100, + .load_config = 1, + .position_resume = 1, + .stream_cache_min_percent = 20.0, + .stream_cache_seek_min_percent = 50.0, + .stream_cache_pause = 10.0, + .chapterrange = {-1, -1}, + .edition_id = -1, + .default_max_pts_correction = -1, + .correct_pts = 1, + .initial_audio_sync = 1, + .term_osd = 2, + .consolecontrols = 1, + .play_frames = -1, + .keep_open = 0, + .audio_id = -1, + .video_id = -1, + .sub_id = -1, + .audio_display = 1, + .sub_visibility = 1, + .sub_pos = 100, + .sub_speed = 1.0, + .audio_output_channels = MP_CHMAP_INIT_STEREO, + .audio_output_format = -1, // AF_FORMAT_UNKNOWN + .playback_speed = 1., + .movie_aspect = -1., + .field_dominance = -1, + .sub_auto = 1, + .osd_bar_visible = 1, +#ifdef CONFIG_ASS + .ass_enabled = 1, +#endif + .sub_scale = 1, + .ass_vsfilter_aspect_compat = 1, + .ass_vsfilter_color_compat = 1, + .ass_vsfilter_blur_compat = 1, + .ass_style_override = 1, + .use_embedded_fonts = 1, + .suboverlap_enabled = 0, + + .hwdec_codecs = "all", + + .index_mode = -1, + + .ad_lavc_param = { + .ac3drc = 1., + .downmix = 1, + }, + .lavfdopts = { + .allow_mimetype = 1, + }, + .input = { + .key_fifo_size = 7, + .doubleclick_time = 300, + .ar_delay = 200, + .ar_rate = 40, + .use_joystick = 1, + .use_lirc = 1, + .use_lircc = 1, +#ifdef CONFIG_COCOA + .use_ar = 1, + .use_media_keys = 1, +#endif + .default_bindings = 1, + }, +}; + +#endif /* MPLAYER_CFG_MPLAYER_H */ diff --git a/mpvcore/options.h b/mpvcore/options.h new file mode 100644 index 0000000000..c83ab7a73a --- /dev/null +++ b/mpvcore/options.h @@ -0,0 +1,276 @@ +#ifndef MPLAYER_OPTIONS_H +#define MPLAYER_OPTIONS_H + +#include <stdbool.h> +#include <stdint.h> +#include "core/m_option.h" + +typedef struct mp_vo_opts { + struct m_obj_settings *video_driver_list; + + int screenwidth; + int screenheight; + int ontop; + int fullscreen; + int screen_id; + int fsscreen_id; + char *winname; + char** fstype_list; + int native_keyrepeat; + + float panscan; + float panscanrange; + + struct m_geometry geometry; + struct m_geometry autofit; + struct m_geometry autofit_larger; + + int fsmode; + int keepaspect; + int border; + + int nomouse_input; + int enable_mouse_movements; + int cursor_autohide_delay; + + int64_t WinID; + + float force_monitor_aspect; + float monitor_pixel_aspect; + int force_window_position; + + int native_fs; +} mp_vo_opts; + +typedef struct MPOpts { + char **reset_options; + + struct m_obj_settings *audio_driver_list; + int fixed_vo; + int softvol; + float mixer_init_volume; + int mixer_init_mute; + int volstep; + float softvol_max; + int gapless_audio; + + mp_vo_opts vo; + + char *wintitle; + int force_rgba_osd; + + // ranges -100 - 100, 1000 if the vo default should be used + int gamma_gamma; + int gamma_brightness; + int gamma_contrast; + int gamma_saturation; + int gamma_hue; + + int stop_screensaver; + int requested_colorspace; + int requested_input_range; + int requested_output_range; + + char *audio_decoders; + char *video_decoders; + + int osd_level; + int osd_duration; + int osd_fractions; + int untimed; + char *stream_capture; + char *stream_dump; + int loop_times; + int ordered_chapters; + int chapter_merge_threshold; + int quiet; + int load_config; + int use_filedir_conf; + int stream_cache_size; + int stream_cache_def_size; + float stream_cache_min_percent; + float stream_cache_seek_min_percent; + int stream_cache_pause; + int chapterrange[2]; + int edition_id; + int correct_pts; + int user_pts_assoc_mode; + int initial_audio_sync; + int hr_seek; + float hr_seek_demuxer_offset; + float audio_delay; + float default_max_pts_correction; + int autosync; + int softsleep; + int frame_dropping; + int term_osd; + char *term_osd_esc; + char *playing_msg; + char *status_msg; + char *osd_status_msg; + char *heartbeat_cmd; + float heartbeat_interval; + int player_idle_mode; + int slave_mode; + int consolecontrols; + int list_properties; + struct m_rel_time play_start; + struct m_rel_time play_end; + struct m_rel_time play_length; + int play_frames; + double step_sec; + int64_t seek_to_byte; + int position_resume; + int position_save_on_quit; + int pause; + int keep_open; + int audio_id; + int video_id; + int sub_id; + char **audio_lang; + char **sub_lang; + int audio_display; + int sub_visibility; + int sub_pos; + float sub_delay; + float sub_fps; + float sub_speed; + int forced_subs_only; + char *quvi_format; + + // subreader.c + int suboverlap_enabled; + char *sub_cp; + + char *audio_stream; + int audio_stream_cache; + char *demuxer_name; + char *audio_demuxer_name; + char *sub_demuxer_name; + int mkv_subtitle_preroll; + + struct image_writer_opts *screenshot_image_opts; + char *screenshot_template; + + double force_fps; + int index_mode; // -1=untouched 0=don't use index 1=use (generate) index + + struct mp_chmap audio_output_channels; + int audio_output_format; + int force_srate; + int dtshd; + double playback_speed; + struct m_obj_settings *vf_settings; + struct m_obj_settings *af_settings; + float movie_aspect; + int flip; + int field_dominance; + int divx_quality; + char **sub_name; + char **sub_paths; + int sub_auto; + int sub_match_fuzziness; + int osd_bar_visible; + float osd_bar_align_x; + float osd_bar_align_y; + float osd_bar_w; + float osd_bar_h; + float osd_scale; + struct osd_style_opts *osd_style; + struct osd_style_opts *sub_text_style; + float sub_scale; + float sub_gauss; + int sub_gray; + int ass_enabled; + float ass_line_spacing; + int ass_use_margins; + int ass_vsfilter_aspect_compat; + int ass_vsfilter_color_compat; + int ass_vsfilter_blur_compat; + int use_embedded_fonts; + char **ass_force_style_list; + char *ass_styles_file; + int ass_style_override; + int ass_hinting; + + int hwdec_api; + char *hwdec_codecs; + + struct lavc_param { + int fast; + char *skip_loop_filter_str; + char *skip_idct_str; + char *skip_frame_str; + int threads; + int bitexact; + char *avopt; + } lavc_param; + + struct ad_lavc_param { + float ac3drc; + int downmix; + char *avopt; + } ad_lavc_param; + + struct lavfdopts { + int probesize; + int probescore; + float analyzeduration; + int buffersize; + int allow_mimetype; + char *format; + char *cryptokey; + char *avopt; + int genptsmode; + } lavfdopts; + + struct input_conf { + char *config_file; + int doubleclick_time; + int key_fifo_size; + int ar_delay; + int ar_rate; + char *js_dev; + char *in_file; + int use_joystick; + int use_lirc; + int use_lircc; + int use_ar; + int use_media_keys; + int default_bindings; + int test; + } input; + + struct encode_output_conf { + char *file; + char *format; + char **fopts; + float fps; + float maxfps; + char *vcodec; + char **vopts; + char *acodec; + char **aopts; + int harddup; + float voffset; + float aoffset; + int copyts; + int rawts; + int autofps; + int neverdrop; + int video_first; + int audio_first; + } encode_output; +} MPOpts; + +// Should be moved into MPOpts +extern char **network_http_header_fields; +extern char *network_useragent; +extern char *network_referrer; +extern int network_cookies_enabled; +extern char *cookies_file; + +extern const m_option_t mp_opts[]; +extern const struct MPOpts mp_default_opts; + +#endif diff --git a/mpvcore/parser-cfg.c b/mpvcore/parser-cfg.c new file mode 100644 index 0000000000..a69f4545d2 --- /dev/null +++ b/mpvcore/parser-cfg.c @@ -0,0 +1,249 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <ctype.h> +#include <assert.h> + +#include "osdep/io.h" + +#include "parser-cfg.h" +#include "core/mp_msg.h" +#include "core/m_option.h" +#include "m_config.h" + +/// Maximal include depth. +#define MAX_RECURSION_DEPTH 8 + +/// Current include depth. +static int recursion_depth = 0; + +/// Setup the \ref Config from a config file. +/** \param config The config object. + * \param conffile Path to the config file. + * \param flags M_SETOPT_* bits + * \return 1 on sucess, -1 on error, 0 if file not accessible. + */ +int m_config_parse_config_file(m_config_t *config, const char *conffile, + int flags) +{ +#define PRINT_LINENUM mp_msg(MSGT_CFGPARSER, MSGL_ERR, "%s:%d: ", conffile, line_num) +#define MAX_LINE_LEN 10000 +#define MAX_OPT_LEN 1000 +#define MAX_PARAM_LEN 1500 + FILE *fp = NULL; + char *line = NULL; + char opt[MAX_OPT_LEN + 1]; + char param[MAX_PARAM_LEN + 1]; + char c; /* for the "" and '' check */ + int tmp; + int line_num = 0; + int line_pos; /* line pos */ + int opt_pos; /* opt pos */ + int param_pos; /* param pos */ + int ret = 1; + int errors = 0; + m_profile_t *profile = NULL; + + flags = flags | M_SETOPT_FROM_CONFIG_FILE; + + mp_msg(MSGT_CFGPARSER, MSGL_V, "Reading config file %s", conffile); + + if (recursion_depth > MAX_RECURSION_DEPTH) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + ": too deep 'include'. check your configfiles\n"); + ret = -1; + goto out; + } + + if ((line = malloc(MAX_LINE_LEN + 1)) == NULL) { + mp_msg(MSGT_CFGPARSER, MSGL_FATAL, + "\ncan't get memory for 'line': %s", strerror(errno)); + ret = -1; + goto out; + } else + + mp_msg(MSGT_CFGPARSER, MSGL_V, "\n"); + + if ((fp = fopen(conffile, "r")) == NULL) { + mp_msg(MSGT_CFGPARSER, MSGL_V, ": %s\n", strerror(errno)); + ret = 0; + goto out; + } + + while (fgets(line, MAX_LINE_LEN, fp)) { + if (errors >= 16) { + mp_msg(MSGT_CFGPARSER, MSGL_FATAL, "too many errors\n"); + goto out; + } + + line_num++; + line_pos = 0; + + /* skip whitespaces */ + while (isspace(line[line_pos])) + ++line_pos; + + /* EOL / comment */ + if (line[line_pos] == '\0' || line[line_pos] == '#') + continue; + + /* read option. */ + for (opt_pos = 0; isprint(line[line_pos]) && + line[line_pos] != ' ' && + line[line_pos] != '#' && + line[line_pos] != '='; /* NOTHING */) { + opt[opt_pos++] = line[line_pos++]; + if (opt_pos >= MAX_OPT_LEN) { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "too long option\n"); + errors++; + ret = -1; + goto nextline; + } + } + if (opt_pos == 0) { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "parse error\n"); + ret = -1; + errors++; + continue; + } + opt[opt_pos] = '\0'; + + /* Profile declaration */ + if (opt_pos > 2 && opt[0] == '[' && opt[opt_pos - 1] == ']') { + opt[opt_pos - 1] = '\0'; + if (strcmp(opt + 1, "default")) + profile = m_config_add_profile(config, opt + 1); + else + profile = NULL; + continue; + } + + /* skip whitespaces */ + while (isspace(line[line_pos])) + ++line_pos; + + param_pos = 0; + bool param_set = false; + + /* check '=' */ + if (line[line_pos] == '=') { + line_pos++; + param_set = true; + + /* whitespaces... */ + while (isspace(line[line_pos])) + ++line_pos; + + /* read the parameter */ + if (line[line_pos] == '"' || line[line_pos] == '\'') { + c = line[line_pos]; + ++line_pos; + for (param_pos = 0; line[line_pos] != c; /* NOTHING */) { + param[param_pos++] = line[line_pos++]; + if (param_pos >= MAX_PARAM_LEN) { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "option %s has a too long parameter\n", opt); + ret = -1; + errors++; + goto nextline; + } + } + line_pos++; /* skip the closing " or ' */ + } else { + for (param_pos = 0; isprint(line[line_pos]) + && !isspace(line[line_pos]) + && line[line_pos] != '#'; /* NOTHING */) { + param[param_pos++] = line[line_pos++]; + if (param_pos >= MAX_PARAM_LEN) { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, "too long parameter\n"); + ret = -1; + errors++; + goto nextline; + } + } + } + + while (isspace(line[line_pos])) + ++line_pos; + } + param[param_pos] = '\0'; + + /* EOL / comment */ + if (line[line_pos] != '\0' && line[line_pos] != '#') { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "extra characters: %s\n", line + line_pos); + ret = -1; + } + + bstr bopt = bstr0(opt); + bstr bparam = bstr0(param); + + if (profile && bstr_equals0(bopt, "profile-desc")) { + m_profile_set_desc(profile, param); + goto nextline; + } + + tmp = m_config_option_requires_param(config, bopt); + if (tmp > 0 && !param_set) + tmp = M_OPT_MISSING_PARAM; + if (tmp < 0) { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "error parsing option %s=%s: %s\n", + opt, param, m_option_strerror(tmp)); + continue; + } + + if (profile) { + tmp = m_config_set_profile_option(config, profile, bopt, bparam); + } else { + tmp = m_config_set_option_ext(config, bopt, bparam, flags); + } + if (tmp < 0) { + PRINT_LINENUM; + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "setting option %s='%s' failed.\n", opt, param); + continue; + /* break */ + } +nextline: + ; + } + +out: + free(line); + if (fp) + fclose(fp); + --recursion_depth; + if (ret < 0) { + mp_msg(MSGT_CFGPARSER, MSGL_FATAL, "Error loading config file %s.\n", + conffile); + } + return ret; +} diff --git a/mpvcore/parser-cfg.h b/mpvcore/parser-cfg.h new file mode 100644 index 0000000000..65a4b49496 --- /dev/null +++ b/mpvcore/parser-cfg.h @@ -0,0 +1,27 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_PARSER_CFG_H +#define MPLAYER_PARSER_CFG_H + +#include "m_config.h" + +int m_config_parse_config_file(m_config_t* config, const char *conffile, + int flags); + +#endif /* MPLAYER_PARSER_CFG_H */ diff --git a/mpvcore/parser-mpcmd.c b/mpvcore/parser-mpcmd.c new file mode 100644 index 0000000000..d716fc4d28 --- /dev/null +++ b/mpvcore/parser-mpcmd.c @@ -0,0 +1,314 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> +#include <assert.h> +#include <stdbool.h> + +#include "core/mp_msg.h" +#include "core/m_option.h" +#include "m_config.h" +#include "playlist.h" +#include "playlist_parser.h" +#include "parser-mpcmd.h" + +#define GLOBAL 0 +#define LOCAL 1 + +#define dvd_range(a) (a > 0 && a < 256) + + +struct parse_state { + struct m_config *config; + int argc; + char **argv; + + bool no_more_opts; + bool error; + + bool is_opt; + struct bstr arg; + struct bstr param; +}; + +// Returns 0 if a valid option/file is available, <0 on error, 1 on end of args. +static int split_opt_silent(struct parse_state *p) +{ + assert(!p->error); + + if (p->argc < 1) + return 1; + + p->is_opt = false; + p->arg = bstr0(p->argv[0]); + p->param = bstr0(NULL); + + p->argc--; + p->argv++; + + if (p->no_more_opts || !bstr_startswith0(p->arg, "-") || p->arg.len == 1) + return 0; + + if (bstrcmp0(p->arg, "--") == 0) { + p->no_more_opts = true; + return split_opt_silent(p); + } + + p->is_opt = true; + + if (!bstr_eatstart0(&p->arg, "--")) + bstr_eatstart0(&p->arg, "-"); + + bool ambiguous = !bstr_split_tok(p->arg, "=", &p->arg, &p->param); + + int r = m_config_option_requires_param(p->config, p->arg); + if (r < 0) + return r; + + if (ambiguous && r > 0) { + if (p->argc < 1) + return M_OPT_MISSING_PARAM; + p->param = bstr0(p->argv[0]); + p->argc--; + p->argv++; + } + + return 0; +} + +// Returns true if more args, false if all parsed or an error occurred. +static bool split_opt(struct parse_state *p) +{ + int r = split_opt_silent(p); + if (r >= 0) + return r == 0; + p->error = true; + + mp_tmsg(MSGT_CFGPARSER, MSGL_FATAL, + "Error parsing commandline option %.*s: %s\n", + BSTR_P(p->arg), m_option_strerror(r)); + return false; +} + +static bool parse_flag(bstr name, bstr f) +{ + struct m_option opt = {NULL, NULL, CONF_TYPE_FLAG, 0, 0, 1, NULL}; + int val = 0; + m_option_parse(&opt, name, f, &val); + return !!val; +} + +// returns M_OPT_... error code +int m_config_parse_mp_command_line(m_config_t *config, struct playlist *files, + int argc, char **argv) +{ + int ret = M_OPT_UNKNOWN; + int mode = 0; + struct playlist_entry *local_start = NULL; + bool shuffle = false; + + int local_params_count = 0; + struct playlist_param *local_params = 0; + + assert(config != NULL); + + mode = GLOBAL; + + struct parse_state p = {config, argc, argv}; + while (split_opt(&p)) { + if (p.is_opt) { + int flags = mode == LOCAL ? M_SETOPT_BACKUP | M_SETOPT_CHECK_ONLY : 0; + int r; + r = m_config_set_option_ext(config, p.arg, p.param, flags); + if (r <= M_OPT_EXIT) { + ret = r; + goto err_out; + } + if (r < 0) { + mp_tmsg(MSGT_CFGPARSER, MSGL_FATAL, + "Setting commandline option --%.*s=%.*s failed.\n", + BSTR_P(p.arg), BSTR_P(p.param)); + goto err_out; + } + + // Handle some special arguments outside option parser. + + if (!bstrcmp0(p.arg, "{")) { + if (mode != GLOBAL) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "'--{' can not be nested.\n"); + goto err_out; + } + mode = LOCAL; + assert(!local_start); + local_start = files->last; + continue; + } + + if (!bstrcmp0(p.arg, "}")) { + if (mode != LOCAL) { + mp_msg(MSGT_CFGPARSER, MSGL_ERR, + "Too many closing '--}'.\n"); + goto err_out; + } + if (local_params_count) { + // The files added between '{' and '}' are the entries from + // the entry _after_ local_start, until the end of the list. + // If local_start is NULL, the list was empty on '{', and we + // want all files in the list. + struct playlist_entry *cur + = local_start ? local_start->next : files->first; + if (!cur) + mp_msg(MSGT_CFGPARSER, MSGL_WARN, "Ignored options!\n"); + while (cur) { + playlist_entry_add_params(cur, local_params, + local_params_count); + cur = cur->next; + } + } + local_params_count = 0; + mode = GLOBAL; + m_config_restore_backups(config); + local_start = NULL; + shuffle = false; + continue; + } + + if (bstrcmp0(p.arg, "shuffle") == 0) { + shuffle = parse_flag(p.arg, p.param); + continue; + } + if (bstrcmp0(p.arg, "no-shuffle") == 0) { + shuffle = false; + continue; + } + + if (bstrcmp0(p.arg, "playlist") == 0) { + // append the playlist to the local args + char *param0 = bstrdup0(NULL, p.param); + struct playlist *pl = playlist_parse_file(param0); + talloc_free(param0); + if (!pl) { + mp_tmsg(MSGT_CFGPARSER, MSGL_FATAL, + "Error reading playlist '%.*s'", BSTR_P(p.param)); + goto err_out; + } + playlist_transfer_entries(files, pl); + talloc_free(pl); + continue; + } + + if (mode == LOCAL) { + MP_TARRAY_APPEND(NULL, local_params, local_params_count, + (struct playlist_param) {p.arg, p.param}); + } + } else { + // filename + void *tmp = talloc_new(NULL); + bstr file = p.arg; + char *file0 = bstrdup0(tmp, p.arg); + // expand DVD filename entries like dvd://1-3 into component titles + if (bstr_startswith0(file, "dvd://")) { + int offset = 6; + char *splitpos = strstr(file0 + offset, "-"); + if (splitpos != NULL) { + char *endpos; + int start_title = strtol(file0 + offset, &endpos, 10); + int end_title; + //entries like dvd://-2 imply start at title 1 + if (start_title < 0) { + end_title = abs(start_title); + start_title = 1; + } else + end_title = strtol(splitpos + 1, &endpos, 10); + + if (dvd_range(start_title) && dvd_range(end_title) + && (start_title < end_title)) { + for (int j = start_title; j <= end_title; j++) { + char *f = talloc_asprintf(tmp, "dvd://%d%s", j, + endpos); + playlist_add_file(files, f); + } + } else + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, + "Invalid play entry %s\n", file0); + + } else // dvd:// or dvd://x entry + playlist_add_file(files, file0); + } else + playlist_add_file(files, file0); + talloc_free(tmp); + + // Lock stdin if it will be used as input + if (bstrcmp0(file, "-") == 0) + m_config_set_option0(config, "consolecontrols", "no"); + } + } + + if (p.error) + goto err_out; + + if (mode != GLOBAL) { + mp_tmsg(MSGT_CFGPARSER, MSGL_ERR, + "Missing closing --} on command line.\n"); + goto err_out; + } + + if (shuffle) + playlist_shuffle(files); + + ret = 0; // success + +err_out: + talloc_free(local_params); + m_config_restore_backups(config); + return ret; +} + +extern int mp_msg_levels[]; + +/* Parse some command line options early before main parsing. + * --no-config prevents reading configuration files (otherwise done before + * command line parsing), and --really-quiet suppresses messages printed + * during normal options parsing. + */ +void m_config_preparse_command_line(m_config_t *config, int argc, char **argv) +{ + // Hack to shut up parser error messages + int msg_lvl_backup = mp_msg_levels[MSGT_CFGPARSER]; + mp_msg_levels[MSGT_CFGPARSER] = -11; + + struct parse_state p = {config, argc, argv}; + while (split_opt_silent(&p) == 0) { + if (p.is_opt) { + // Ignore non-pre-parse options. They will be set later. + // Option parsing errors will be handled later as well. + m_config_set_option_ext(config, p.arg, p.param, + M_SETOPT_PRE_PARSE_ONLY); + if (bstrcmp0(p.arg, "v") == 0) + verbose++; + } + } + + mp_msg_levels[MSGT_CFGPARSER] = msg_lvl_backup; +} diff --git a/mpvcore/parser-mpcmd.h b/mpvcore/parser-mpcmd.h new file mode 100644 index 0000000000..256bc514b2 --- /dev/null +++ b/mpvcore/parser-mpcmd.h @@ -0,0 +1,33 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_PARSER_MPCMD_H +#define MPLAYER_PARSER_MPCMD_H + +#include <stdbool.h> + +struct playlist; +struct m_config; + +int m_config_parse_mp_command_line(struct m_config *config, + struct playlist *files, + int argc, char **argv); +void m_config_preparse_command_line(struct m_config *config, + int argc, char **argv); + +#endif /* MPLAYER_PARSER_MPCMD_H */ diff --git a/mpvcore/path.c b/mpvcore/path.c new file mode 100644 index 0000000000..50350be18c --- /dev/null +++ b/mpvcore/path.c @@ -0,0 +1,229 @@ +/* + * Get path to config dir/file. + * + * Return Values: + * Returns the pointer to the ALLOCATED buffer containing the + * zero terminated path string. This buffer has to be FREED + * by the caller. + * + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <assert.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stdbool.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <errno.h> +#include "config.h" +#include "core/mp_msg.h" +#include "core/path.h" +#include "talloc.h" +#include "osdep/io.h" + +#if defined(__MINGW32__) +#include <windows.h> +#elif defined(__CYGWIN__) +#include <windows.h> +#include <sys/cygwin.h> +#endif + +#ifdef CONFIG_MACOSX_BUNDLE +#include "osdep/macosx_bundle.h" +#endif + + +typedef char *(*lookup_fun)(const char *); +static const lookup_fun config_lookup_functions[] = { + mp_find_user_config_file, +#ifdef CONFIG_MACOSX_BUNDLE + get_bundled_path, +#endif + mp_find_global_config_file, + NULL +}; + +char *mp_find_config_file(const char *filename) +{ + for (int i = 0; config_lookup_functions[i] != NULL; i++) { + char *path = config_lookup_functions[i](filename); + if (!path) continue; + + if (mp_path_exists(path)) + return path; + + talloc_free(path); + } + return NULL; +} + +char *mp_find_user_config_file(const char *filename) +{ + char *homedir = NULL, *buff = NULL; +#ifdef __MINGW32__ + static char *config_dir = "mpv"; +#else + static char *config_dir = ".mpv"; +#endif +#if defined(__MINGW32__) || defined(__CYGWIN__) + char *temp = NULL; + char exedir[260]; + /* Hack to get fonts etc. loaded outside of Cygwin environment. */ + int i, imax = 0; + int len = (int)GetModuleFileNameA(NULL, exedir, 260); + for (i = 0; i < len; i++) + if (exedir[i] == '\\') { + exedir[i] = '/'; + imax = i; + } + exedir[imax] = '\0'; + + if (filename) + temp = mp_path_join(NULL, bstr0(exedir), bstr0(filename)); + + if (temp && mp_path_exists(temp) && !mp_path_isdir(temp)) { + homedir = exedir; + config_dir = ""; + } + else +#endif + if ((homedir = getenv("MPV_HOME")) != NULL) { + config_dir = ""; + } else if ((homedir = getenv("HOME")) == NULL) { +#if defined(__MINGW32__) || defined(__CYGWIN__) + homedir = exedir; +#else + return NULL; +#endif + } +#if defined(__MINGW32__) || defined(__CYGWIN__) + talloc_free(temp); +#endif + + if (filename) { + char * temp = mp_path_join(NULL, bstr0(homedir), bstr0(config_dir)); + buff = mp_path_join(NULL, bstr0(temp), bstr0(filename)); + talloc_free(temp); + } else { + buff = mp_path_join(NULL, bstr0(homedir), bstr0(config_dir)); + } + + mp_msg(MSGT_GLOBAL, MSGL_V, "get_path('%s') -> '%s'\n", filename, buff); + return buff; +} + +char *mp_find_global_config_file(const char *filename) +{ + if (filename) { + return mp_path_join(NULL, bstr0(MPLAYER_CONFDIR), bstr0(filename)); + } else { + return talloc_strdup(NULL, MPLAYER_CONFDIR); + } +} + +char *mp_basename(const char *path) +{ + char *s; + +#if HAVE_DOS_PATHS + s = strrchr(path, '\\'); + if (s) + path = s + 1; + s = strrchr(path, ':'); + if (s) + path = s + 1; +#endif + s = strrchr(path, '/'); + return s ? s + 1 : (char *)path; +} + +struct bstr mp_dirname(const char *path) +{ + struct bstr ret = { + (uint8_t *)path, mp_basename(path) - path + }; + if (ret.len == 0) + return bstr0("."); + return ret; +} + +char *mp_splitext(const char *path, bstr *root) +{ + assert(path); + const char *split = strrchr(path, '.'); + if (!split) + split = path + strlen(path); + if (root) + *root = (bstr){.start = (char *)path, .len = path - split}; + return (char *)split; +} + +char *mp_path_join(void *talloc_ctx, struct bstr p1, struct bstr p2) +{ + if (p1.len == 0) + return bstrdup0(talloc_ctx, p2); + if (p2.len == 0) + return bstrdup0(talloc_ctx, p1); + +#if HAVE_DOS_PATHS + if (p2.len >= 2 && p2.start[1] == ':' + || p2.start[0] == '\\' || p2.start[0] == '/') +#else + if (p2.start[0] == '/') +#endif + return bstrdup0(talloc_ctx, p2); // absolute path + + bool have_separator; + int endchar1 = p1.start[p1.len - 1]; +#if HAVE_DOS_PATHS + have_separator = endchar1 == '/' || endchar1 == '\\' + || p1.len == 2 && endchar1 == ':'; // "X:" only +#else + have_separator = endchar1 == '/'; +#endif + + return talloc_asprintf(talloc_ctx, "%.*s%s%.*s", BSTR_P(p1), + have_separator ? "" : "/", BSTR_P(p2)); +} + +char *mp_getcwd(void *talloc_ctx) +{ + char *wd = talloc_array(talloc_ctx, char, 20); + while (getcwd(wd, talloc_get_size(wd)) == NULL) { + if (errno != ERANGE) { + talloc_free(wd); + return NULL; + } + wd = talloc_realloc(talloc_ctx, wd, char, talloc_get_size(wd) * 2); + } + return wd; +} + +bool mp_path_exists(const char *path) +{ + struct stat st; + return mp_stat(path, &st) == 0; +} + +bool mp_path_isdir(const char *path) +{ + struct stat st; + return mp_stat(path, &st) == 0 && S_ISDIR(st.st_mode); +} diff --git a/mpvcore/path.h b/mpvcore/path.h new file mode 100644 index 0000000000..a38ad503ea --- /dev/null +++ b/mpvcore/path.h @@ -0,0 +1,66 @@ +/* + * Get path to config dir/file. + * + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_PATH_H +#define MPLAYER_PATH_H + +#include <stdbool.h> +#include "core/bstr.h" + + +// Search for the input filename in several paths. These include user and global +// config locations by default. Some platforms may implement additional platform +// related lookups (i.e.: OSX inside an application bundle). +char *mp_find_config_file(const char *filename); + +// Search for the input filename in the global configuration location. +char *mp_find_global_config_file(const char *filename); + +// Search for the input filename in the user configuration location. +char *mp_find_user_config_file(const char *filename); + +// Return pointer to filename part of path + +char *mp_basename(const char *path); + +/* Return file extension, including the '.'. If root is not NULL, set it to the + * part of the path without extension. So: path == root + returnvalue + * Don't consider it a file extension if the only '.' is the first character. + * Return "" if no extension. + */ +char *mp_splitext(const char *path, bstr *root); + +/* Return struct bstr referencing directory part of path, or if that + * would be empty, ".". + */ +struct bstr mp_dirname(const char *path); + +/* Join two path components and return a newly allocated string + * for the result. '/' is inserted between the components if needed. + * If p2 is an absolute path then the value of p1 is ignored. + */ +char *mp_path_join(void *talloc_ctx, struct bstr p1, struct bstr p2); + +char *mp_getcwd(void *talloc_ctx); + +bool mp_path_exists(const char *path); +bool mp_path_isdir(const char *path); + +#endif /* MPLAYER_PATH_H */ diff --git a/mpvcore/playlist.c b/mpvcore/playlist.c new file mode 100644 index 0000000000..b016cebca6 --- /dev/null +++ b/mpvcore/playlist.c @@ -0,0 +1,246 @@ +/* + * This file is part of mplayer. + * + * mplayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mplayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mplayer. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <assert.h> +#include "config.h" +#include "playlist.h" +#include "core/mp_common.h" +#include "talloc.h" +#include "core/path.h" + +struct playlist_entry *playlist_entry_new(const char *filename) +{ + struct playlist_entry *e = talloc_zero(NULL, struct playlist_entry); + e->filename = talloc_strdup(e, filename); + return e; +} + +void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value) +{ + struct playlist_param p = {bstrdup(e, name), bstrdup(e, value)}; + MP_TARRAY_APPEND(e, e->params, e->num_params, p); +} + +void playlist_entry_add_params(struct playlist_entry *e, + struct playlist_param *params, + int num_params) +{ + for (int n = 0; n < num_params; n++) + playlist_entry_add_param(e, params[n].name, params[n].value); +} + +// Add entry "add" after entry "after". +// If "after" is NULL, add as first entry. +// Post condition: add->prev == after +void playlist_insert(struct playlist *pl, struct playlist_entry *after, + struct playlist_entry *add) +{ + assert(pl && add->pl == NULL && add->next == NULL && add->prev == NULL); + if (after) { + assert(after->pl == pl); + assert(pl->first && pl->last); + } + add->prev = after; + if (after) { + add->next = after->next; + after->next = add; + } else { + add->next = pl->first; + pl->first = add; + } + if (add->next) { + add->next->prev = add; + } else { + pl->last = add; + } + add->pl = pl; + talloc_steal(pl, add); +} + +void playlist_add(struct playlist *pl, struct playlist_entry *add) +{ + playlist_insert(pl, pl->last, add); +} + +static void playlist_unlink(struct playlist *pl, struct playlist_entry *entry) +{ + assert(pl && entry->pl == pl); + + if (pl->current == entry) { + pl->current = entry->next; + pl->current_was_replaced = true; + } + + if (entry->next) { + entry->next->prev = entry->prev; + } else { + pl->last = entry->prev; + } + if (entry->prev) { + entry->prev->next = entry->next; + } else { + pl->first = entry->next; + } + entry->next = entry->prev = NULL; + // xxx: we'd want to reset the talloc parent of entry + entry->pl = NULL; +} + +void playlist_remove(struct playlist *pl, struct playlist_entry *entry) +{ + playlist_unlink(pl, entry); + talloc_free(entry); +} + +void playlist_clear(struct playlist *pl) +{ + while (pl->first) + playlist_remove(pl, pl->first); + assert(!pl->current); + pl->current_was_replaced = false; +} + +// Moves entry such that entry->prev = at (even if at is NULL) +void playlist_move(struct playlist *pl, struct playlist_entry *entry, + struct playlist_entry *at) +{ + struct playlist_entry *save_current = pl->current; + bool save_replaced = pl->current_was_replaced; + + playlist_unlink(pl, entry); + playlist_insert(pl, at ? at->prev : pl->last, entry); + + pl->current = save_current; + pl->current_was_replaced = save_replaced; +} + +void playlist_add_file(struct playlist *pl, const char *filename) +{ + playlist_add(pl, playlist_entry_new(filename)); +} + +static int playlist_count(struct playlist *pl) +{ + int c = 0; + for (struct playlist_entry *e = pl->first; e; e = e->next) + c++; + return c; +} + +void playlist_shuffle(struct playlist *pl) +{ + struct playlist_entry *save_current = pl->current; + bool save_replaced = pl->current_was_replaced; + int count = playlist_count(pl); + struct playlist_entry **arr = talloc_array(NULL, struct playlist_entry *, + count); + for (int n = 0; n < count; n++) { + arr[n] = pl->first; + playlist_unlink(pl, pl->first); + } + for (int n = 0; n < count; n++) { + int other = (int)((double)(count) * rand() / (RAND_MAX + 1.0)); + struct playlist_entry *tmp = arr[n]; + arr[n] = arr[other]; + arr[other] = tmp; + } + for (int n = 0; n < count; n++) + playlist_add(pl, arr[n]); + talloc_free(arr); + pl->current = save_current; + pl->current_was_replaced = save_replaced; +} + +struct playlist_entry *playlist_get_next(struct playlist *pl, int direction) +{ + assert(direction == -1 || direction == +1); + if (!pl->current) + return NULL; + assert(pl->current->pl == pl); + if (direction < 0) + return pl->current->prev; + return pl->current_was_replaced ? pl->current : pl->current->next; +} + +static bool might_be_an_url(bstr f) +{ + return bstr_find0(f, "://") >= 0; +} + +void playlist_add_base_path(struct playlist *pl, bstr base_path) +{ + if (base_path.len == 0 || bstrcmp0(base_path, ".") == 0) + return; + for (struct playlist_entry *e = pl->first; e; e = e->next) { + if (!might_be_an_url(bstr0(e->filename))) { + char *new_file = mp_path_join(e, base_path, bstr0(e->filename)); + talloc_free(e->filename); + e->filename = new_file; + } + } +} + +// Move all entries from source_pl to pl, appending them after the current entry +// of pl. source_pl will be empty, and all entries have changed ownership to pl. +void playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl) +{ + struct playlist_entry *add_after = pl->current; + if (pl->current && pl->current_was_replaced) + add_after = pl->current->next; + if (!add_after) + add_after = pl->last; + + while (source_pl->first) { + struct playlist_entry *e = source_pl->first; + playlist_unlink(source_pl, e); + playlist_insert(pl, add_after, e); + add_after = e; + } +} + +// Return number of entries between list start and e. +// Return -1 if e is not on the list, or if e is NULL. +int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e) +{ + struct playlist_entry *cur = pl->first; + int pos = 0; + if (!e) + return -1; + while (cur && cur != e) { + cur = cur->next; + pos++; + } + return cur == e ? pos : -1; +} + +int playlist_entry_count(struct playlist *pl) +{ + return playlist_entry_to_index(pl, pl->last) + 1; +} + +// Return entry for which playlist_entry_to_index() would return index. +// Return NULL if not found. +struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index) +{ + struct playlist_entry *e = pl->first; + for (int n = 0; ; n++) { + if (!e || n == index) + return e; + e = e->next; + } +} + diff --git a/mpvcore/playlist.h b/mpvcore/playlist.h new file mode 100644 index 0000000000..f01d4b8ddd --- /dev/null +++ b/mpvcore/playlist.h @@ -0,0 +1,74 @@ +/* + * This file is part of mplayer. + * + * mplayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mplayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mplayer. If not, see <http://www.gnu.org/licenses/>. + */ + +#ifndef MPLAYER_PLAYLIST_H +#define MPLAYER_PLAYLIST_H + +#include <stdbool.h> +#include "core/bstr.h" + +struct playlist_param { + bstr name, value; +}; + +struct playlist_entry { + struct playlist_entry *prev, *next; + struct playlist *pl; + + char *filename; + + struct playlist_param *params; + int num_params; +}; + +struct playlist { + struct playlist_entry *first, *last; + + // This provides some sort of stable iterator. If this entry is removed from + // the playlist, current is set to the next element (or NULL), and + // current_was_replaced is set to true. + struct playlist_entry *current; + bool current_was_replaced; +}; + +void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value); +void playlist_entry_add_params(struct playlist_entry *e, + struct playlist_param *params, + int params_count); + +struct playlist_entry *playlist_entry_new(const char *filename); + +void playlist_insert(struct playlist *pl, struct playlist_entry *after, + struct playlist_entry *add); +void playlist_add(struct playlist *pl, struct playlist_entry *add); +void playlist_remove(struct playlist *pl, struct playlist_entry *entry); +void playlist_clear(struct playlist *pl); + +void playlist_move(struct playlist *pl, struct playlist_entry *entry, + struct playlist_entry *at); + +void playlist_add_file(struct playlist *pl, const char *filename); +void playlist_shuffle(struct playlist *pl); +struct playlist_entry *playlist_get_next(struct playlist *pl, int direction); +void playlist_add_base_path(struct playlist *pl, bstr base_path); +void playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl); + +int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e); +int playlist_entry_count(struct playlist *pl); +struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index); + +#endif diff --git a/mpvcore/playlist_parser.c b/mpvcore/playlist_parser.c new file mode 100644 index 0000000000..59d5123be6 --- /dev/null +++ b/mpvcore/playlist_parser.c @@ -0,0 +1,777 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "config.h" +#include <stdlib.h> +#include <stdio.h> +#include <string.h> +#include <assert.h> +#include <errno.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <unistd.h> +#include <ctype.h> +#include <limits.h> + +#include "talloc.h" +#include "asxparser.h" +#include "m_config.h" +#include "playlist.h" +#include "playlist_parser.h" +#include "stream/stream.h" +#include "demux/demux.h" +#include "core/mp_msg.h" +#include "core/path.h" + + +#define BUF_STEP 1024 + +#define WHITES " \n\r\t" + +typedef struct play_tree_parser { + struct stream *stream; + char *buffer,*iter,*line; + int buffer_size , buffer_end; + int keep; + struct playlist *pl; +} play_tree_parser_t; + +static void +strstrip(char* str) { + char* i; + + if (str==NULL) + return; + for(i = str ; i[0] != '\0' && strchr(WHITES,i[0]) != NULL; i++) + /* NOTHING */; + if(i[0] != '\0') { + memmove(str,i,strlen(i) + 1); + for(i = str + strlen(str) - 1 ; strchr(WHITES,i[0]) != NULL; i--) + /* NOTHING */; + i[1] = '\0'; + } else + str[0] = '\0'; +} + +static char* +play_tree_parser_get_line(play_tree_parser_t* p) { + char *end,*line_end; + int r,resize = 0; + + if(p->buffer == NULL) { + p->buffer = malloc(BUF_STEP); + p->buffer_size = BUF_STEP; + p->buffer[0] = 0; + p->iter = p->buffer; + } + + if(p->stream->eof && (p->buffer_end == 0 || p->iter[0] == '\0')) + return NULL; + + assert(p->buffer_end < p->buffer_size); + assert(!p->buffer[p->buffer_end]); + while(1) { + + if(resize) { + char *tmp; + r = p->iter - p->buffer; + end = p->buffer + p->buffer_end; + if (p->buffer_size > INT_MAX - BUF_STEP) + break; + tmp = realloc(p->buffer, p->buffer_size + BUF_STEP); + if (!tmp) + break; + p->buffer = tmp; + p->iter = p->buffer + r; + p->buffer_size += BUF_STEP; + resize = 0; + } + + if(p->buffer_size - p->buffer_end > 1 && ! p->stream->eof) { + r = stream_read(p->stream,p->buffer + p->buffer_end,p->buffer_size - p->buffer_end - 1); + if(r > 0) { + p->buffer_end += r; + assert(p->buffer_end < p->buffer_size); + p->buffer[p->buffer_end] = '\0'; + while(strlen(p->buffer + p->buffer_end - r) != r) + p->buffer[p->buffer_end - r + strlen(p->buffer + p->buffer_end - r)] = '\n'; + } + assert(!p->buffer[p->buffer_end]); + } + + end = strchr(p->iter,'\n'); + if(!end) { + if(p->stream->eof) { + end = p->buffer + p->buffer_end; + break; + } + resize = 1; + continue; + } + break; + } + + line_end = (end > p->iter && *(end-1) == '\r') ? end-1 : end; + if(line_end - p->iter >= 0) + p->line = realloc(p->line, line_end - p->iter + 1); + else + return NULL; + if(line_end - p->iter > 0) + strncpy(p->line,p->iter,line_end - p->iter); + p->line[line_end - p->iter] = '\0'; + if(end[0] != '\0') + end++; + + if(!p->keep) { + if(end[0] != '\0') { + p->buffer_end -= end-p->iter; + memmove(p->buffer,end,p->buffer_end); + } else + p->buffer_end = 0; + p->buffer[p->buffer_end] = '\0'; + p->iter = p->buffer; + } else + p->iter = end; + + return p->line; +} + +static void +play_tree_parser_reset(play_tree_parser_t* p) { + p->iter = p->buffer; +} + +static void +play_tree_parser_stop_keeping(play_tree_parser_t* p) { + p->keep = 0; + if(p->iter && p->iter != p->buffer) { + p->buffer_end -= p->iter -p->buffer; + if(p->buffer_end) + memmove(p->buffer,p->iter,p->buffer_end); + p->buffer[p->buffer_end] = 0; + p->iter = p->buffer; + } +} + + +static bool parse_asx(play_tree_parser_t* p) { + int comments = 0,get_line = 1; + char* line = NULL; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying asx...\n"); + + while(1) { + if(get_line) { + line = play_tree_parser_get_line(p); + if(!line) + return false; + strstrip(line); + if(line[0] == '\0') + continue; + } + if(!comments) { + if(line[0] != '<') { + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"First char isn't '<' but '%c'\n",line[0]); + mp_msg(MSGT_PLAYTREE,MSGL_DBG3,"Buffer = [%s]\n",p->buffer); + return false; + } else if(strncmp(line,"<!--",4) == 0) { // Comments + comments = 1; + line += 4; + if(line[0] != '\0' && strlen(line) > 0) + get_line = 0; + } else if(strncasecmp(line,"<ASX",4) == 0) // We got an asx element + break; + else // We don't get an asx + return false; + } else { // Comments + char* c; + c = strchr(line,'-'); + if(c) { + if (strncmp(c,"--!>",4) == 0) { // End of comments + comments = 0; + line = c+4; + if(line[0] != '\0') // There is some more data on this line : keep it + get_line = 0; + + } else { + line = c+1; // Jump the - + if(line[0] != '\0') // Some more data + get_line = 0; + else // End of line + get_line = 1; + } + } else // No - on this line (or rest of line) : get next one + get_line = 1; + } + } + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected asx format\n"); + + // We have an asx : load it in memory and parse + + while((line = play_tree_parser_get_line(p)) != NULL) + /* NOTHING */; + + mp_msg(MSGT_PLAYTREE,MSGL_DBG3,"Parsing asx file: [%s]\n",p->buffer); + return asx_parse(p->buffer,p->pl); +} + +static char* +pls_entry_get_value(char* line) { + char* i; + + i = strchr(line,'='); + if(!i || i[1] == '\0') + return NULL; + else + return i+1; +} + +typedef struct pls_entry { + char* file; + char* title; + char* length; +} pls_entry_t; + +static int +pls_read_entry(char* line,pls_entry_t** _e,int* _max_entry,char** val) { + int num,max_entry = (*_max_entry); + pls_entry_t* e = (*_e); + int limit = INT_MAX / sizeof(*e); + char* v; + + v = pls_entry_get_value(line); + if(!v) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"No value in entry %s\n",line); + return -1; + } + + num = atoi(line); + if(num <= 0 || num > limit) { + if (max_entry >= limit) { + mp_msg(MSGT_PLAYTREE, MSGL_WARN, "Too many index entries\n"); + return -1; + } + num = max_entry+1; + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"No or invalid entry index in entry %s\nAssuming %d\n",line,num); + } + if(num > max_entry) { + e = realloc(e, num * sizeof(pls_entry_t)); + if (!e) + return -1; + memset(&e[max_entry],0,(num-max_entry)*sizeof(pls_entry_t)); + max_entry = num; + } + (*_e) = e; + (*_max_entry) = max_entry; + (*val) = v; + + return num; +} + + +static bool parse_pls(play_tree_parser_t* p) { + char *line,*v; + pls_entry_t* entries = NULL; + int n_entries = 0,max_entry=0,num; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying Winamp playlist...\n"); + while((line = play_tree_parser_get_line(p))) { + strstrip(line); + if(strlen(line)) + break; + } + if (!line) + return false; + if(strcasecmp(line,"[playlist]")) + return false; + mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected Winamp playlist format\n"); + play_tree_parser_stop_keeping(p); + line = play_tree_parser_get_line(p); + if(!line) + return false; + strstrip(line); + if(strncasecmp(line,"NumberOfEntries",15) == 0) { + v = pls_entry_get_value(line); + n_entries = atoi(v); + if(n_entries < 0) + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Invalid number of entries: very funny!!!\n"); + else + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Playlist claims to have %d entries. Let's see.\n",n_entries); + line = play_tree_parser_get_line(p); + } + + while(line) { + strstrip(line); + if(line[0] == '\0') { + line = play_tree_parser_get_line(p); + continue; + } + if(strncasecmp(line,"File",4) == 0) { + num = pls_read_entry(line+4,&entries,&max_entry,&v); + if(num < 0) + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"No value in entry %s\n",line); + else + entries[num-1].file = strdup(v); + } else if(strncasecmp(line,"Title",5) == 0) { + num = pls_read_entry(line+5,&entries,&max_entry,&v); + if(num < 0) + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"No value in entry %s\n",line); + else + entries[num-1].title = strdup(v); + } else if(strncasecmp(line,"Length",6) == 0) { + num = pls_read_entry(line+6,&entries,&max_entry,&v); + if(num < 0) + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"No value in entry %s\n",line); + else { + char *end; + long val = strtol(v, &end, 10); + if (*end || (val <= 0 && val != -1)) + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"Invalid length value in entry %s\n",line); + else if (val > 0) + entries[num-1].length = strdup(v); + } + } else + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"Unknown entry type %s\n",line); + line = play_tree_parser_get_line(p); + } + + for(num = 0; num < max_entry ; num++) { + if(entries[num].file == NULL) + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"Entry %d don't have a file !!!!\n",num+1); + else { + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Adding entry %s\n",entries[num].file); + playlist_add_file(p->pl,entries[num].file); + if (entries[num].length) + playlist_entry_add_param(p->pl->last, bstr0("end"), bstr0(entries[num].length)); + free(entries[num].file); + } + // When we have info in playtree we add these info + free(entries[num].title); + free(entries[num].length); + } + + free(entries); + return true; +} + +/* + Reference Ini-Format: Each entry is assumed a reference + */ +static bool parse_ref_ini(play_tree_parser_t* p) { + char *line,*v; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying reference-ini playlist...\n"); + if (!(line = play_tree_parser_get_line(p))) + return NULL; + strstrip(line); + if(strcasecmp(line,"[Reference]")) + return NULL; + mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected reference-ini playlist format\n"); + play_tree_parser_stop_keeping(p); + line = play_tree_parser_get_line(p); + if(!line) + return NULL; + while(line) { + strstrip(line); + if(strncasecmp(line,"Ref",3) == 0) { + v = pls_entry_get_value(line+3); + if(!v) + mp_msg(MSGT_PLAYTREE,MSGL_ERR,"No value in entry %s\n",line); + else + { + mp_msg(MSGT_PLAYTREE,MSGL_DBG2,"Adding entry %s\n",v); + playlist_add_file(p->pl, v); + } + } + line = play_tree_parser_get_line(p); + } + + return true; +} + +static bool parse_m3u(play_tree_parser_t* p) { + char* line; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying extended m3u playlist...\n"); + if (!(line = play_tree_parser_get_line(p))) + return NULL; + strstrip(line); + if(strcasecmp(line,"#EXTM3U")) + return NULL; + mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected extended m3u playlist format\n"); + play_tree_parser_stop_keeping(p); + + while((line = play_tree_parser_get_line(p)) != NULL) { + strstrip(line); + if(line[0] == '\0') + continue; + /* EXTM3U files contain such lines: + * #EXTINF:<seconds>, <title> + * followed by a line with the filename + * for now we have no place to put that + * so we just skip that extra-info ::atmos + */ + if(line[0] == '#') { +#if 0 /* code functional */ + if(strncasecmp(line,"#EXTINF:",8) == 0) { + mp_msg(MSGT_PLAYTREE,MSGL_INFO,"[M3U] Duration: %dsec Title: %s\n", + strtol(line+8,&line,10), line+2); + } +#endif + continue; + } + playlist_add_file(p->pl, line); + } + + return true; +} + +static bool parse_smil(play_tree_parser_t* p) { + int entrymode=0; + char* line,source[512],*pos,*s_start,*s_end,*src_line; + int is_rmsmil = 0; + unsigned int npkt, ttlpkt; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying smil playlist...\n"); + + // Check if smil + while((line = play_tree_parser_get_line(p)) != NULL) { + strstrip(line); + if(line[0] == '\0') // Ignore empties + continue; + if (strncasecmp(line,"<?xml",5)==0) // smil in xml + continue; + if (strncasecmp(line,"<!DOCTYPE smil",13)==0) // smil in xml + continue; + if (strncasecmp(line,"<smil",5)==0 || strncasecmp(line,"<?wpl",5)==0 || + strncasecmp(line,"(smil-document",14)==0) + break; // smil header found + else + return NULL; //line not smil exit + } + + if (!line) return NULL; + mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected smil playlist format\n"); + play_tree_parser_stop_keeping(p); + + if (strncasecmp(line,"(smil-document",14)==0) { + mp_msg(MSGT_PLAYTREE,MSGL_V,"Special smil-over-realrtsp playlist header\n"); + is_rmsmil = 1; + if (sscanf(line, "(smil-document (ver 1.0)(npkt %u)(ttlpkt %u", &npkt, &ttlpkt) != 2) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"smil-over-realrtsp: header parsing failure, assuming single packet.\n"); + npkt = ttlpkt = 1; + } + if (ttlpkt == 0 || npkt > ttlpkt) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"smil-over-realrtsp: bad packet counters (npkk = %u, ttlpkt = %u), assuming single packet.\n", + npkt, ttlpkt); + npkt = ttlpkt = 1; + } + } + + //Get entries from smil + src_line = line; + line = NULL; + do { + strstrip(src_line); + free(line); + line = NULL; + /* If we're parsing smil over realrtsp and this is not the last packet and + * this is the last line in the packet (terminating with ") ) we must get + * the next line, strip the header, and concatenate it to the current line. + */ + if (is_rmsmil && npkt != ttlpkt && strstr(src_line,"\")")) { + char *payload; + + line = strdup(src_line); + if(!(src_line = play_tree_parser_get_line(p))) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"smil-over-realrtsp: can't get line from packet %u/%u.\n", npkt, ttlpkt); + break; + } + strstrip(src_line); + // Skip header, packet starts after " + if(!(payload = strchr(src_line,'\"'))) { + mp_msg(MSGT_PLAYTREE,MSGL_WARN,"smil-over-realrtsp: can't find start of packet, using complete line.\n"); + payload = src_line; + } else + payload++; + // Skip ") at the end of the last line from the current packet + line[strlen(line)-2] = 0; + line = realloc(line, strlen(line)+strlen(payload)+1); + strcat (line, payload); + npkt++; + } else + line = strdup(src_line); + /* Unescape \" to " for smil-over-rtsp */ + if (is_rmsmil && line[0] != '\0') { + int i, j; + + for (i = 0; i < strlen(line); i++) + if (line[i] == '\\' && line[i+1] == '"') + for (j = i; line[j]; j++) + line[j] = line[j+1]; + } + pos = line; + while (pos) { + if (!entrymode) { // all entries filled so far + while ((pos=strchr(pos, '<'))) { + if (strncasecmp(pos,"<video",6)==0 || strncasecmp(pos,"<audio",6)==0 || strncasecmp(pos,"<media",6)==0) { + entrymode=1; + break; // Got a valid tag, exit '<' search loop + } + pos++; + } + } + if (entrymode) { //Entry found but not yet filled + pos = strstr(pos,"src="); // Is source present on this line + if (pos != NULL) { + entrymode=0; + if (pos[4] != '"' && pos[4] != '\'') { + mp_msg(MSGT_PLAYTREE,MSGL_V,"Unknown delimiter %c in source line %s\n", pos[4], line); + break; + } + s_start=pos+5; + s_end=strchr(s_start,pos[4]); + if (s_end == NULL) { + mp_msg(MSGT_PLAYTREE,MSGL_V,"Error parsing this source line %s\n",line); + break; + } + if (s_end-s_start> 511) { + mp_msg(MSGT_PLAYTREE,MSGL_V,"Cannot store such a large source %s\n",line); + break; + } + strncpy(source,s_start,s_end-s_start); + source[(s_end-s_start)]='\0'; // Null terminate + playlist_add_file(p->pl, source); + pos = s_end; + } + } + } + } while((src_line = play_tree_parser_get_line(p)) != NULL); + + free(line); + return true; +} + +static bool parse_textplain(play_tree_parser_t* p) { + char* line; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying plaintext playlist...\n"); + play_tree_parser_stop_keeping(p); + + while((line = play_tree_parser_get_line(p)) != NULL) { + strstrip(line); + if(line[0] == '\0' || line[0] == '#' || (line[0] == '/' && line[1] == '/')) + continue; + + playlist_add_file(p->pl,line); + } + + return true; +} + +/** + * \brief decode the base64 used in nsc files + * \param in input string, 0-terminated + * \param buf output buffer, must point to memory suitable for realloc, + * will be NULL on failure. + * \return decoded length in bytes + */ +static int decode_nsc_base64(char *in, char **buf) { + int i, j, n; + if (in[0] != '0' || in[1] != '2') + goto err_out; + in += 2; // skip prefix + if (strlen(in) < 16) // error out if nothing to decode + goto err_out; + in += 12; // skip encoded string length + n = strlen(in) / 4; + *buf = realloc(*buf, n * 3); + for (i = 0; i < n; i++) { + uint8_t c[4]; + for (j = 0; j < 4; j++) { + c[j] = in[4 * i + j]; + if (c[j] >= '0' && c[j] <= '9') c[j] += 0 - '0'; + else if (c[j] >= 'A' && c[j] <= 'Z') c[j] += 10 - 'A'; + else if (c[j] >= 'a' && c[j] <= 'z') c[j] += 36 - 'a'; + else if (c[j] == '{') c[j] = 62; + else if (c[j] == '}') c[j] = 63; + else { + mp_msg(MSGT_PLAYTREE, MSGL_ERR, "Invalid character %c (0x%02"PRIx8")\n", c[j], c[j]); + goto err_out; + } + } + (*buf)[3 * i] = (c[0] << 2) | (c[1] >> 4); + (*buf)[3 * i + 1] = (c[1] << 4) | (c[2] >> 2); + (*buf)[3 * i + 2] = (c[2] << 6) | c[3]; + } + return 3 * n; +err_out: + free(*buf); + *buf = NULL; + return 0; +} + +/** + * \brief "converts" utf16 to ascii by just discarding every second byte + * \param buf buffer to convert + * \param len lenght of buffer, must be > 0 + */ +static void utf16_to_ascii(char *buf, int len) { + int i; + if (len <= 0) return; + for (i = 0; i < len / 2; i++) + buf[i] = buf[i * 2]; + buf[i] = 0; // just in case +} + +static bool parse_nsc(play_tree_parser_t* p) { + char *line, *addr = NULL, *url, *unicast_url = NULL; + int port = 0; + + mp_msg(MSGT_PLAYTREE,MSGL_V,"Trying nsc playlist...\n"); + while((line = play_tree_parser_get_line(p)) != NULL) { + strstrip(line); + if(!line[0]) // Ignore empties + continue; + if (strncasecmp(line,"[Address]", 9) == 0) + break; // nsc header found + else + return false; + } + mp_msg(MSGT_PLAYTREE,MSGL_V,"Detected nsc playlist format\n"); + play_tree_parser_stop_keeping(p); + while ((line = play_tree_parser_get_line(p)) != NULL) { + strstrip(line); + if (!line[0]) + continue; + if (strncasecmp(line, "Unicast URL=", 12) == 0) { + int len = decode_nsc_base64(&line[12], &unicast_url); + if (len <= 0) + mp_msg(MSGT_PLAYTREE, MSGL_WARN, "[nsc] Unsupported Unicast URL encoding\n"); + else + utf16_to_ascii(unicast_url, len); + } else if (strncasecmp(line, "IP Address=", 11) == 0) { + int len = decode_nsc_base64(&line[11], &addr); + if (len <= 0) + mp_msg(MSGT_PLAYTREE, MSGL_WARN, "[nsc] Unsupported IP Address encoding\n"); + else + utf16_to_ascii(addr, len); + } else if (strncasecmp(line, "IP Port=", 8) == 0) { + port = strtol(&line[8], NULL, 0); + } + } + + bool success = false; + + if (unicast_url) + url = strdup(unicast_url); + else if (addr && port) { + url = malloc(strlen(addr) + 7 + 20 + 1); + sprintf(url, "http://%s:%i", addr, port); + } else + goto err_out; + + playlist_add_file(p->pl, url); + free(url); + success = true; +err_out: + free(addr); + free(unicast_url); + return success; +} + +struct playlist *playlist_parse_file(const char *file) +{ + stream_t *stream = stream_open(file, NULL); + if(!stream) { + mp_msg(MSGT_PLAYTREE,MSGL_ERR, + "Error while opening playlist file %s: %s\n", + file, strerror(errno)); + return false; + } + + mp_msg(MSGT_PLAYTREE, MSGL_V, + "Parsing playlist file %s...\n", file); + + struct playlist *ret = playlist_parse(stream); + free_stream(stream); + + playlist_add_base_path(ret, mp_dirname(file)); + + return ret; + +} + +typedef bool (*parser_fn)(play_tree_parser_t *); +static const parser_fn pl_parsers[] = { + parse_asx, + parse_pls, + parse_m3u, + parse_ref_ini, + parse_smil, + parse_nsc, + parse_textplain +}; + + +static struct playlist *do_parse(struct stream* stream, bool forced) +{ + play_tree_parser_t p = { + .stream = stream, + .pl = talloc_zero(NULL, struct playlist), + .keep = 1, + }; + + bool success = false; + if (play_tree_parser_get_line(&p) != NULL) { + for (int n = 0; n < sizeof(pl_parsers) / sizeof(pl_parsers[0]); n++) { + play_tree_parser_reset(&p); + if (pl_parsers[n] == parse_textplain && !forced) + break; + if (pl_parsers[n](&p)) { + success = true; + break; + } + } + } + + if(success) + mp_msg(MSGT_PLAYTREE,MSGL_V,"Playlist successfully parsed\n"); + else { + mp_msg(MSGT_PLAYTREE,((forced==1)?MSGL_ERR:MSGL_V),"Error while parsing playlist\n"); + talloc_free(p.pl); + p.pl = NULL; + } + + if (p.pl && !p.pl->first) + mp_msg(MSGT_PLAYTREE,((forced==1)?MSGL_WARN:MSGL_V),"Warning: empty playlist\n"); + + return p.pl; +} + +struct playlist *playlist_parse(struct stream* stream) +{ + return do_parse(stream, true); +} + +struct playlist *playlist_probe_and_parse(struct stream* stream) +{ + return do_parse(stream, false); +} diff --git a/mpvcore/playlist_parser.h b/mpvcore/playlist_parser.h new file mode 100644 index 0000000000..3ceb95c460 --- /dev/null +++ b/mpvcore/playlist_parser.h @@ -0,0 +1,34 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_PLAYLISTPARSER_H +#define MPLAYER_PLAYLISTPARSER_H + +#include <stdbool.h> + +struct stream; +struct playlist; + +// Parse the given stream as playlist. Append entries to pl. Return whether +// there was an error when parsing. +// deep = Parser depth. Some formats allow including other files, +struct playlist *playlist_parse(struct stream* stream); +struct playlist *playlist_probe_and_parse(struct stream* stream); +struct playlist *playlist_parse_file(const char *file); + +#endif diff --git a/mpvcore/resolve.h b/mpvcore/resolve.h new file mode 100644 index 0000000000..91684df250 --- /dev/null +++ b/mpvcore/resolve.h @@ -0,0 +1,53 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MP_RESOLVE_H +#define MP_RESOLVE_H + +struct MPContext; +struct MPOpts; + +struct mp_resolve_result { + char *url; + char *title; + + struct mp_resolve_src **srcs; + int num_srcs; + + double start_time; + + struct mp_resolve_sub **subs; + int num_subs; + + struct playlist *playlist; +}; + +struct mp_resolve_src { + char *url; + char *encid; // indicates quality level, contents are libquvi specific +}; + +struct mp_resolve_sub { + char *url; + char *data; + char *lang; +}; + +struct mp_resolve_result *mp_resolve_quvi(const char *url, struct MPOpts *opts); + +#endif diff --git a/mpvcore/resolve_quvi.c b/mpvcore/resolve_quvi.c new file mode 100644 index 0000000000..5870335811 --- /dev/null +++ b/mpvcore/resolve_quvi.c @@ -0,0 +1,93 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <quvi/quvi.h> + +#include "talloc.h" +#include "core/mp_msg.h" +#include "core/options.h" +#include "resolve.h" + +struct mp_resolve_result *mp_resolve_quvi(const char *url, struct MPOpts *opts) +{ + QUVIcode rc; + bool mp_url = false; + + quvi_t q; + rc = quvi_init(&q); + if (rc != QUVI_OK) + return NULL; + + if (!strncmp(url, "mp_", 3)) { + url += 3; + mp_url = true; + } + + // Don't try to use quvi on an URL that's not directly supported, since + // quvi will do a network access anyway in order to check for HTTP + // redirections etc. + // The documentation says this will fail on "shortened" URLs. + if (quvi_supported(q, (char *)url) != QUVI_OK) { + quvi_close(&q); + return NULL; + } + + mp_msg(MSGT_OPEN, MSGL_INFO, "[quvi] Checking URL...\n"); + + // Can use quvi_query_formats() to get a list of formats like this: + // "fmt05_240p|fmt18_360p|fmt34_360p|fmt35_480p|fmt43_360p|fmt44_480p" + // (This example is youtube specific.) + // That call requires an extra net access. quvi_next_media_url() doesn't + // seem to do anything useful. So we can't really do anything useful + // except pass through the user's format setting. + quvi_setopt(q, QUVIOPT_FORMAT, opts->quvi_format + ? opts->quvi_format : "best"); + + quvi_media_t m; + rc = quvi_parse(q, (char *)url, &m); + if (rc != QUVI_OK) { + mp_msg(MSGT_OPEN, MSGL_ERR, "[quvi] %s\n", quvi_strerror(q, rc)); + quvi_close(&q); + return NULL; + } + + struct mp_resolve_result *result + = talloc_zero(NULL, struct mp_resolve_result); + + char *val; + + if (quvi_getprop(m, QUVIPROP_MEDIAURL, &val) == QUVI_OK) { + if (mp_url) + result->url = talloc_asprintf(result, "mp_%s", val); + else + result->url = talloc_strdup(result, val); + } + + if (quvi_getprop(m, QUVIPROP_PAGETITLE, &val) == QUVI_OK) + result->title = talloc_strdup(result, val); + + quvi_parse_close(&m); + quvi_close(&q); + + if (!result->url) { + talloc_free(result); + result = NULL; + } + + return result; +} diff --git a/mpvcore/resolve_quvi9.c b/mpvcore/resolve_quvi9.c new file mode 100644 index 0000000000..f6e6e8b94f --- /dev/null +++ b/mpvcore/resolve_quvi9.c @@ -0,0 +1,150 @@ +/* + * This file is part of mpv. + * + * mpv is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mpv 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mpv. If not, see <http://www.gnu.org/licenses/>. + */ + +#include <stdbool.h> +#include <assert.h> + +#include <quvi.h> + +#include "talloc.h" +#include "core/mp_msg.h" +#include "core/options.h" +#include "core/playlist.h" +#include "resolve.h" + +static bool mp_quvi_ok(quvi_t q) +{ + if (!quvi_ok(q)) { + mp_msg(MSGT_OPEN, MSGL_ERR, "[quvi] %s\n", quvi_errmsg(q)); + return false; + } + return true; +} + +struct mp_resolve_result *mp_resolve_quvi(const char *url, struct MPOpts *opts) +{ + int mode = QUVI_SUPPORTS_MODE_OFFLINE; + + quvi_t q = quvi_new(); + if (!quvi_ok(q)) { + mp_msg(MSGT_OPEN, MSGL_ERR, "[quvi] %s\n", quvi_errmsg(q)); + + quvi_free(q); + return NULL; + } + + struct mp_resolve_result *res = talloc_zero(NULL, struct mp_resolve_result); + + if (quvi_supports(q, url, mode, QUVI_SUPPORTS_TYPE_PLAYLIST)) { + mp_msg(MSGT_OPEN, MSGL_INFO, "[quvi] Checking playlist...\n"); + quvi_playlist_t qp = quvi_playlist_new(q, url); + if (mp_quvi_ok(q)) { + res->playlist = talloc_zero(res, struct playlist); + while (quvi_playlist_media_next(qp)) { + char *entry = NULL; + quvi_playlist_get(qp, QUVI_PLAYLIST_MEDIA_PROPERTY_URL, &entry); + if (entry) + playlist_add_file(res->playlist, entry); + } + } + quvi_playlist_free(qp); + } + + if (quvi_supports(q, url, mode, QUVI_SUPPORTS_TYPE_MEDIA)) { + mp_msg(MSGT_OPEN, MSGL_INFO, "[quvi] Checking URL...\n"); + quvi_media_t media = quvi_media_new(q, url); + if (mp_quvi_ok(q)) { + char *format = opts->quvi_format ? opts->quvi_format : "best"; + bool use_default = strcmp(format, "default") == 0; + if (!use_default) + quvi_media_stream_select(media, format); + + char *val = NULL; + quvi_media_get(media, QUVI_MEDIA_STREAM_PROPERTY_URL, &val); + res->url = talloc_strdup(res, val); + + val = NULL; + quvi_media_get(media, QUVI_MEDIA_PROPERTY_TITLE, &val); + res->title = talloc_strdup(res, val); + + double start = 0; + quvi_media_get(media, QUVI_MEDIA_PROPERTY_START_TIME_MS, &start); + res->start_time = start / 1000.0; + + quvi_media_stream_reset(media); + while (quvi_media_stream_next(media)) { + char *entry = NULL, *id = NULL; + quvi_media_get(media, QUVI_MEDIA_STREAM_PROPERTY_URL, &entry); + quvi_media_get(media, QUVI_MEDIA_STREAM_PROPERTY_ID, &id); + if (entry) { + struct mp_resolve_src *src = talloc_ptrtype(res, src); + *src = (struct mp_resolve_src) { + .url = talloc_strdup(src, entry), + .encid = talloc_strdup(src, id), + }; + MP_TARRAY_APPEND(res, res->srcs, res->num_srcs, src); + talloc_steal(res->srcs, src); + } + } + + } + quvi_media_free(media); + } + + if (quvi_supports(q, url, mode, QUVI_SUPPORTS_TYPE_SUBTITLE)) { + mp_msg(MSGT_OPEN, MSGL_INFO, "[quvi] Getting subtitles...\n"); + quvi_subtitle_t qsub = quvi_subtitle_new(q, url); + if (mp_quvi_ok(q)) { + while (1) { + quvi_subtitle_type_t qst = quvi_subtitle_type_next(qsub); + if (!qst) + break; + while (1) { + quvi_subtitle_lang_t qsl = quvi_subtitle_lang_next(qst); + if (!qsl) + break; + char *lang; + quvi_subtitle_lang_get(qsl, QUVI_SUBTITLE_LANG_PROPERTY_ID, + &lang); + // Let quvi convert the subtitle to SRT. + quvi_subtitle_export_t qse = + quvi_subtitle_export_new(qsl, "srt"); + if (mp_quvi_ok(q)) { + const char *subdata = quvi_subtitle_export_data(qse); + struct mp_resolve_sub *sub = talloc_ptrtype(res, sub); + *sub = (struct mp_resolve_sub) { + .lang = talloc_strdup(sub, lang), + .data = talloc_strdup(sub, subdata), + }; + MP_TARRAY_APPEND(res, res->subs, res->num_subs, sub); + talloc_steal(res->subs, sub); + } + quvi_subtitle_export_free(qse); + } + } + } + quvi_subtitle_free(qsub); + } + + quvi_free(q); + + if (!res->url && (!res->playlist || !res->playlist->first)) { + talloc_free(res); + res = NULL; + } + return res; +} diff --git a/mpvcore/screenshot.c b/mpvcore/screenshot.c new file mode 100644 index 0000000000..ea2fe7a3c9 --- /dev/null +++ b/mpvcore/screenshot.c @@ -0,0 +1,390 @@ +/* + * This file is part of mplayer2. + * + * mplayer2 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mplayer2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mplayer2; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdlib.h> +#include <string.h> +#include <time.h> + +#include "config.h" + +#include "osdep/io.h" + +#include "talloc.h" +#include "core/screenshot.h" +#include "core/mp_core.h" +#include "core/command.h" +#include "core/bstr.h" +#include "core/mp_msg.h" +#include "core/mp_osd.h" +#include "core/path.h" +#include "video/mp_image.h" +#include "video/decode/dec_video.h" +#include "video/filter/vf.h" +#include "video/out/vo.h" +#include "video/image_writer.h" +#include "sub/sub.h" + +#include "video/csputils.h" + +#define MODE_FULL_WINDOW 1 +#define MODE_SUBTITLES 2 + +typedef struct screenshot_ctx { + struct MPContext *mpctx; + + int mode; + bool each_frame; + bool osd; + + int frameno; +} screenshot_ctx; + +void screenshot_init(struct MPContext *mpctx) +{ + mpctx->screenshot_ctx = talloc(mpctx, screenshot_ctx); + *mpctx->screenshot_ctx = (screenshot_ctx) { + .mpctx = mpctx, + .frameno = 1, + }; +} + +#define SMSG_OK 0 +#define SMSG_ERR 1 + +static void screenshot_msg(screenshot_ctx *ctx, int status, const char *msg, + ...) PRINTF_ATTRIBUTE(3,4); + +static void screenshot_msg(screenshot_ctx *ctx, int status, const char *msg, + ...) +{ + va_list ap; + char *s; + + va_start(ap, msg); + s = talloc_vasprintf(NULL, msg, ap); + va_end(ap); + + mp_msg(MSGT_CPLAYER, status == SMSG_ERR ? MSGL_ERR : MSGL_INFO, "%s\n", s); + if (ctx->osd) { + set_osd_tmsg(ctx->mpctx, OSD_MSG_TEXT, 1, ctx->mpctx->opts->osd_duration, + "%s", s); + } + + talloc_free(s); +} + +static char *stripext(void *talloc_ctx, const char *s) +{ + const char *end = strrchr(s, '.'); + if (!end) + end = s + strlen(s); + return talloc_asprintf(talloc_ctx, "%.*s", (int)(end - s), s); +} + +#ifdef _WIN32 +#define ILLEGAL_FILENAME_CHARS "?\"/\\<>*|:" +#else +#define ILLEGAL_FILENAME_CHARS "/" +#endif + +// Replace all characters disallowed in filenames with '_' and return the newly +// allocated result string. +static char *sanitize_filename(void *talloc_ctx, const char *s) +{ + char *res = talloc_strdup(talloc_ctx, s); + char *cur = res; + while (*cur) { + if (strchr(ILLEGAL_FILENAME_CHARS, *cur) || ((unsigned char)*cur) < 32) + *cur = '_'; + cur++; + } + return res; +} + +static void append_filename(char **s, const char *f) +{ + char *append = sanitize_filename(NULL, f); + *s = talloc_strdup_append(*s, append); + talloc_free(append); +} + +static char *create_fname(struct MPContext *mpctx, char *template, + const char *file_ext, int *sequence, int *frameno) +{ + char *res = talloc_strdup(NULL, ""); //empty string, non-NULL context + + time_t raw_time = time(NULL); + struct tm *local_time = localtime(&raw_time); + + if (!template || *template == '\0') + template = "shot%n"; + + for (;;) { + char *next = strchr(template, '%'); + if (!next) + break; + res = talloc_strndup_append(res, template, next - template); + template = next + 1; + char fmt = *template++; + switch (fmt) { + case '#': + case '0': + case 'n': { + int digits = '4'; + if (fmt == '#') { + if (!*sequence) { + *frameno = 1; + } + fmt = *template++; + } + if (fmt == '0') { + digits = *template++; + if (digits < '0' || digits > '9') + goto error_exit; + fmt = *template++; + } + if (fmt != 'n') + goto error_exit; + char fmtstr[] = {'%', '0', digits, 'd', '\0'}; + res = talloc_asprintf_append(res, fmtstr, *frameno); + if (*frameno < 100000 - 1) { + (*frameno) += 1; + (*sequence) += 1; + } + break; + } + case 'f': + case 'F': { + char *video_file = mp_basename(mpctx->filename); + if (video_file) { + char *name = video_file; + if (fmt == 'F') + name = stripext(res, video_file); + append_filename(&res, name); + } + break; + } + case 'p': + case 'P': { + char *t = mp_format_time(get_current_time(mpctx), fmt == 'P'); + append_filename(&res, t); + talloc_free(t); + break; + } + case 't': { + char tfmt = *template; + if (!tfmt) + goto error_exit; + template++; + char fmtstr[] = {'%', tfmt, '\0'}; + char buffer[80]; + if (strftime(buffer, sizeof(buffer), fmtstr, local_time) == 0) + buffer[0] = '\0'; + append_filename(&res, buffer); + break; + } + case '{': { + char *end = strchr(template, '}'); + if (!end) + goto error_exit; + struct bstr prop = bstr_splice(bstr0(template), 0, end - template); + char *tmp = talloc_asprintf(NULL, "${%.*s}", BSTR_P(prop)); + char *s = mp_property_expand_string(mpctx, tmp); + talloc_free(tmp); + if (s) + append_filename(&res, s); + talloc_free(s); + template = end + 1; + break; + } + case '%': + res = talloc_strdup_append(res, "%"); + break; + default: + goto error_exit; + } + } + + res = talloc_strdup_append(res, template); + return talloc_asprintf_append(res, ".%s", file_ext); + +error_exit: + talloc_free(res); + return NULL; +} + +static char *gen_fname(screenshot_ctx *ctx, const char *file_ext) +{ + int sequence = 0; + for (;;) { + int prev_sequence = sequence; + char *fname = create_fname(ctx->mpctx, + ctx->mpctx->opts->screenshot_template, + file_ext, + &sequence, + &ctx->frameno); + + if (!fname) { + screenshot_msg(ctx, SMSG_ERR, "Invalid screenshot filename " + "template! Fix or remove the --screenshot-template " + "option."); + return NULL; + } + + if (!mp_path_exists(fname)) + return fname; + + if (sequence == prev_sequence) { + screenshot_msg(ctx, SMSG_ERR, "Can't save screenshot, file '%s' " + "already exists!", fname); + talloc_free(fname); + return NULL; + } + + talloc_free(fname); + } +} + +static void add_subs(struct MPContext *mpctx, struct mp_image *image) +{ + int d_w = image->display_w ? image->display_w : image->w; + int d_h = image->display_h ? image->display_h : image->h; + + double sar = (double)image->w / image->h; + double dar = (double)d_w / d_h; + struct mp_osd_res res = { + .w = image->w, + .h = image->h, + .display_par = sar / dar, + .video_par = dar / sar, + }; + + osd_draw_on_image(mpctx->osd, res, mpctx->osd->vo_pts, + OSD_DRAW_SUB_ONLY, image); +} + +static void screenshot_save(struct MPContext *mpctx, struct mp_image *image) +{ + screenshot_ctx *ctx = mpctx->screenshot_ctx; + + struct image_writer_opts *opts = mpctx->opts->screenshot_image_opts; + + char *filename = gen_fname(ctx, image_writer_file_ext(opts)); + if (filename) { + screenshot_msg(ctx, SMSG_OK, "Screenshot: '%s'", filename); + if (!write_image(image, opts, filename)) + screenshot_msg(ctx, SMSG_ERR, "Error writing screenshot!"); + talloc_free(filename); + } +} + +static struct mp_image *screenshot_get(struct MPContext *mpctx, int mode) +{ + struct mp_image *image = NULL; + if (mpctx->video_out && mpctx->video_out->config_ok) { + if (mode == MODE_SUBTITLES && mpctx->osd->render_subs_in_filter) + mode = 0; + + struct voctrl_screenshot_args args = + { .full_window = (mode == MODE_FULL_WINDOW) }; + + struct vf_instance *vfilter = mpctx->sh_video->vfilter; + vfilter->control(vfilter, VFCTRL_SCREENSHOT, &args); + + if (!args.out_image) + vo_control(mpctx->video_out, VOCTRL_SCREENSHOT, &args); + + image = args.out_image; + if (image) { + if (mode == MODE_SUBTITLES && !args.has_osd) + add_subs(mpctx, image); + } + } + return image; +} + +void screenshot_to_file(struct MPContext *mpctx, const char *filename, int mode, + bool osd) +{ + screenshot_ctx *ctx = mpctx->screenshot_ctx; + struct image_writer_opts opts = *mpctx->opts->screenshot_image_opts; + bool old_osd = ctx->osd; + ctx->osd = osd; + + if (mp_path_exists(filename)) { + screenshot_msg(ctx, SMSG_ERR, "Screenshot: file '%s' already exists.", + filename); + goto end; + } + char *ext = mp_splitext(filename, NULL); + if (ext) + opts.format = ext + 1; // omit '.' + struct mp_image *image = screenshot_get(mpctx, mode); + if (!image) { + screenshot_msg(ctx, SMSG_ERR, "Taking screenshot failed."); + goto end; + } + screenshot_msg(ctx, SMSG_OK, "Screenshot: '%s'", filename); + if (!write_image(image, &opts, filename)) + screenshot_msg(ctx, SMSG_ERR, "Error writing screenshot!"); + talloc_free(image); + +end: + ctx->osd = old_osd; +} + +void screenshot_request(struct MPContext *mpctx, int mode, bool each_frame, + bool osd) +{ + screenshot_ctx *ctx = mpctx->screenshot_ctx; + + if (mode == MODE_SUBTITLES && mpctx->osd->render_subs_in_filter) + mode = 0; + + if (each_frame) { + ctx->each_frame = !ctx->each_frame; + if (!ctx->each_frame) + return; + } else { + ctx->each_frame = false; + } + + ctx->mode = mode; + ctx->osd = osd; + + struct mp_image *image = screenshot_get(mpctx, mode); + + if (image) { + screenshot_save(mpctx, image); + } else { + screenshot_msg(ctx, SMSG_ERR, "Taking screenshot failed."); + } + + talloc_free(image); +} + +void screenshot_flip(struct MPContext *mpctx) +{ + screenshot_ctx *ctx = mpctx->screenshot_ctx; + + if (!ctx->each_frame) + return; + + ctx->each_frame = false; + screenshot_request(mpctx, ctx->mode, true, ctx->osd); +} diff --git a/mpvcore/screenshot.h b/mpvcore/screenshot.h new file mode 100644 index 0000000000..1b12ac9b73 --- /dev/null +++ b/mpvcore/screenshot.h @@ -0,0 +1,46 @@ +/* + * This file is part of mplayer2. + * + * mplayer2 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mplayer2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mplayer2; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#ifndef MPLAYER_SCREENSHOT_H +#define MPLAYER_SCREENSHOT_H + +#include <stdbool.h> + +struct MPContext; + +// One time initialization at program start. +void screenshot_init(struct MPContext *mpctx); + +// Request a taking & saving a screenshot of the currently displayed frame. +// mode: 0: -, 1: save the actual output window contents, 2: with subtitles. +// each_frame: If set, this toggles per-frame screenshots, exactly like the +// screenshot slave command (MP_CMD_SCREENSHOT). +// osd: show status on OSD +void screenshot_request(struct MPContext *mpctx, int mode, bool each_frame, + bool osd); + +// filename: where to store the screenshot; doesn't try to find an alternate +// name if the file already exists +// mode, osd: same as in screenshot_request() +void screenshot_to_file(struct MPContext *mpctx, const char *filename, int mode, + bool osd); + +// Called by the playback core code when a new frame is displayed. +void screenshot_flip(struct MPContext *mpctx); + +#endif /* MPLAYER_SCREENSHOT_H */ diff --git a/mpvcore/timeline/tl_cue.c b/mpvcore/timeline/tl_cue.c new file mode 100644 index 0000000000..3c4a997982 --- /dev/null +++ b/mpvcore/timeline/tl_cue.c @@ -0,0 +1,419 @@ +/* + * This file is part of mplayer2. + * + * mplayer2 is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * mplayer2 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with mplayer2; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <dirent.h> +#include <stdlib.h> +#include <stdbool.h> +#include <inttypes.h> +#include <ctype.h> + +#include "talloc.h" + +#include "core/mp_core.h" +#include "core/mp_msg.h" +#include "demux/demux.h" +#include "core/path.h" +#include "core/bstr.h" +#include "core/mp_common.h" +#include "stream/stream.h" + +// used by demuxer_cue.c +bool mp_probe_cue(struct bstr data); + +#define SECS_PER_CUE_FRAME (1.0/75.0) + +enum cue_command { + CUE_ERROR = -1, // not a valid CUE command, or an unknown extension + CUE_EMPTY, // line with whitespace only + CUE_UNUSED, // valid CUE command, but ignored by this code + CUE_FILE, + CUE_TRACK, + CUE_INDEX, + CUE_TITLE, +}; + +static const struct { + enum cue_command command; + const char *text; +} cue_command_strings[] = { + { CUE_FILE, "FILE" }, + { CUE_TRACK, "TRACK" }, + { CUE_INDEX, "INDEX" }, + { CUE_TITLE, "TITLE" }, + { CUE_UNUSED, "CATALOG" }, + { CUE_UNUSED, "CDTEXTFILE" }, + { CUE_UNUSED, "FLAGS" }, + { CUE_UNUSED, "ISRC" }, + { CUE_UNUSED, "PERFORMER" }, + { CUE_UNUSED, "POSTGAP" }, + { CUE_UNUSED, "PREGAP" }, + { CUE_UNUSED, "REM" }, + { CUE_UNUSED, "SONGWRITER" }, + { CUE_UNUSED, "MESSAGE" }, + { -1 }, +}; + +struct cue_track { + double pregap_start; // corresponds to INDEX 00 + double start; // corresponds to INDEX 01 + struct bstr filename; + int source; + struct bstr title; +}; + +static enum cue_command read_cmd(struct bstr *data, struct bstr *out_params) +{ + struct bstr line = bstr_strip_linebreaks(bstr_getline(*data, data)); + line = bstr_lstrip(line); + if (line.len == 0) + return CUE_EMPTY; + for (int n = 0; cue_command_strings[n].command != -1; n++) { + struct bstr name = bstr0(cue_command_strings[n].text); + if (bstr_startswith(line, name)) { + struct bstr rest = bstr_cut(line, name.len); + if (rest.len && !strchr(WHITESPACE, rest.start[0])) + continue; + if (out_params) + *out_params = rest; + return cue_command_strings[n].command; + } + } + return CUE_ERROR; +} + +static bool eat_char(struct bstr *data, char ch) +{ + if (data->len && data->start[0] == ch) { + *data = bstr_cut(*data, 1); + return true; + } else { + return false; + } +} + +static struct bstr read_quoted(struct bstr *data) +{ + *data = bstr_lstrip(*data); + if (!eat_char(data, '"')) + return (struct bstr) {0}; + int end = bstrchr(*data, '"'); + if (end < 0) + return (struct bstr) {0}; + struct bstr res = bstr_splice(*data, 0, end); + *data = bstr_cut(*data, end + 1); + return res; +} + +// Read a 2 digit unsigned decimal integer. +// Return -1 on failure. +static int read_int_2(struct bstr *data) +{ + *data = bstr_lstrip(*data); + if (data->len && data->start[0] == '-') + return -1; + struct bstr s = *data; + int res = (int)bstrtoll(s, &s, 10); + if (data->len == s.len || data->len - s.len > 2) + return -1; + *data = s; + return res; +} + +static double read_time(struct bstr *data) +{ + struct bstr s = *data; + bool ok = true; + double t1 = read_int_2(&s); + ok = eat_char(&s, ':') && ok; + double t2 = read_int_2(&s); + ok = eat_char(&s, ':') && ok; + double t3 = read_int_2(&s); + ok = ok && t1 >= 0 && t2 >= 0 && t3 >= 0; + return ok ? t1 * 60.0 + t2 + t3 * SECS_PER_CUE_FRAME : 0; +} + +static struct bstr skip_utf8_bom(struct bstr data) +{ + return bstr_startswith0(data, "\xEF\xBB\xBF") ? bstr_cut(data, 3) : data; +} + +// Check if the text in data is most likely CUE data. This is used by the +// demuxer code to check the file type. +// data is the start of the probed file, possibly cut off at a random point. +bool mp_probe_cue(struct bstr data) +{ + bool valid = false; + data = skip_utf8_bom(data); + for (;;) { + enum cue_command cmd = read_cmd(&data, NULL); + // End reached. Since the line was most likely cut off, don't use the + // result of the last parsing call. + if (data.len == 0) + break; + if (cmd == CUE_ERROR) + return false; + if (cmd != CUE_EMPTY) + valid = true; + } + return valid; +} + +static void add_source(struct MPContext *mpctx, struct demuxer *d) +{ + MP_TARRAY_APPEND(NULL, mpctx->sources, mpctx->num_sources, d); +} + +static bool try_open(struct MPContext *mpctx, char *filename) +{ + struct bstr bfilename = bstr0(filename); + // Avoid trying to open itself or another .cue file. Best would be + // to check the result of demuxer auto-detection, but the demuxer + // API doesn't allow this without opening a full demuxer. + if (bstr_case_endswith(bfilename, bstr0(".cue")) + || bstrcasecmp(bstr0(mpctx->demuxer->filename), bfilename) == 0) + return false; + + struct stream *s = stream_open(filename, mpctx->opts); + if (!s) + return false; + struct demuxer *d = demux_open(s, NULL, NULL, mpctx->opts); + // Since .bin files are raw PCM data with no headers, we have to explicitly + // open them. Also, try to avoid to open files that are most likely not .bin + // files, as that would only play noise. Checking the file extension is + // fragile, but it's about the only way we have. + // TODO: maybe also could check if the .bin file is a multiple of the Audio + // CD sector size (2352 bytes) + if (!d && bstr_case_endswith(bfilename, bstr0(".bin"))) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "CUE: Opening as BIN file!\n"); + d = demux_open(s, "rawaudio", NULL, mpctx->opts); + } + if (d) { + add_source(mpctx, d); + return true; + } + mp_msg(MSGT_CPLAYER, MSGL_ERR, "Could not open source '%s'!\n", filename); + free_stream(s); + return false; +} + +static bool open_source(struct MPContext *mpctx, struct bstr filename) +{ + void *ctx = talloc_new(NULL); + bool res = false; + + struct bstr dirname = mp_dirname(mpctx->demuxer->filename); + + struct bstr base_filename = bstr0(mp_basename(bstrdup0(ctx, filename))); + if (!base_filename.len) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "CUE: Invalid audio filename in .cue file!\n"); + } else { + char *fullname = mp_path_join(ctx, dirname, base_filename); + if (try_open(mpctx, fullname)) { + res = true; + goto out; + } + } + + // Try an audio file with the same name as the .cue file (but different + // extension). + // Rationale: this situation happens easily if the audio file or both files + // are renamed. + + struct bstr cuefile = + bstr_strip_ext(bstr0(mp_basename(mpctx->demuxer->filename))); + + DIR *d = opendir(bstrdup0(ctx, dirname)); + if (!d) + goto out; + struct dirent *de; + while ((de = readdir(d))) { + char *dename0 = de->d_name; + struct bstr dename = bstr0(dename0); + if (bstr_case_startswith(dename, cuefile)) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "CUE: No useful audio filename " + "in .cue file found, trying with '%s' instead!\n", + dename0); + if (try_open(mpctx, mp_path_join(ctx, dirname, dename))) { + res = true; + break; + } + } + } + closedir(d); + +out: + talloc_free(ctx); + if (!res) + mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: Could not open audio file!\n"); + return res; +} + +// return length of the source in seconds, or -1 if unknown +static double source_get_length(struct demuxer *demuxer) +{ + double get_time_ans; + // <= 0 means DEMUXER_CTRL_NOTIMPL or DEMUXER_CTRL_DONTKNOW + if (demuxer && demux_control(demuxer, DEMUXER_CTRL_GET_TIME_LENGTH, + (void *) &get_time_ans) > 0) + { + return get_time_ans; + } else { + return -1; + } +} + +void build_cue_timeline(struct MPContext *mpctx) +{ + void *ctx = talloc_new(NULL); + + struct bstr data = mpctx->demuxer->file_contents; + data = skip_utf8_bom(data); + + struct cue_track *tracks = NULL; + size_t track_count = 0; + + struct bstr filename = {0}; + // Global metadata, and copied into new tracks. + struct cue_track proto_track = {0}; + struct cue_track *cur_track = &proto_track; + + while (data.len) { + struct bstr param; + switch (read_cmd(&data, ¶m)) { + case CUE_ERROR: + mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: error parsing input file!\n"); + goto out; + case CUE_TRACK: { + track_count++; + tracks = talloc_realloc(ctx, tracks, struct cue_track, track_count); + cur_track = &tracks[track_count - 1]; + *cur_track = proto_track; + break; + } + case CUE_TITLE: + cur_track->title = read_quoted(¶m); + break; + case CUE_INDEX: { + int type = read_int_2(¶m); + double time = read_time(¶m); + if (type == 1) { + cur_track->start = time; + cur_track->filename = filename; + } else if (type == 0) { + cur_track->pregap_start = time; + } + break; + } + case CUE_FILE: + // NOTE: FILE comes before TRACK, so don't use cur_track->filename + filename = read_quoted(¶m); + break; + } + } + + if (track_count == 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "CUE: no tracks found!\n"); + goto out; + } + + // Remove duplicate file entries. This might be too sophisticated, since + // CUE files usually use either separate files for every single track, or + // only one file for all tracks. + + struct bstr *files = 0; + size_t file_count = 0; + + for (size_t n = 0; n < track_count; n++) { + struct cue_track *track = &tracks[n]; + track->source = -1; + for (size_t file = 0; file < file_count; file++) { + if (bstrcmp(files[file], track->filename) == 0) { + track->source = file; + break; + } + } + if (track->source == -1) { + file_count++; + files = talloc_realloc(ctx, files, struct bstr, file_count); + files[file_count - 1] = track->filename; + track->source = file_count - 1; + } + } + + add_source(mpctx, mpctx->demuxer); + + for (size_t i = 0; i < file_count; i++) { + if (!open_source(mpctx, files[i])) + goto out; + } + + struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline, + track_count + 1); + struct chapter *chapters = talloc_array_ptrtype(NULL, chapters, + track_count); + double starttime = 0; + for (int i = 0; i < track_count; i++) { + struct demuxer *source = mpctx->sources[1 + tracks[i].source]; + double duration; + if (i + 1 < track_count && tracks[i].source == tracks[i + 1].source) { + duration = tracks[i + 1].start - tracks[i].start; + } else { + duration = source_get_length(source); + // Two cases: 1) last track of a single-file cue, or 2) any track of + // a multi-file cue. We need to do this for 1) only because the + // timeline needs to be terminated with the length of the last + // track. + duration -= tracks[i].start; + } + if (duration < 0) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, + "CUE: Can't get duration of source file!\n"); + // xxx: do something more reasonable + duration = 0.0; + } + timeline[i] = (struct timeline_part) { + .start = starttime, + .source_start = tracks[i].start, + .source = source, + }; + chapters[i] = (struct chapter) { + .start = timeline[i].start, + // might want to include other metadata here + .name = bstrdup0(chapters, tracks[i].title), + }; + starttime += duration; + } + + // apparently we need this to give the last part a non-zero length + timeline[track_count] = (struct timeline_part) { + .start = starttime, + // perhaps unused by the timeline code + .source_start = 0, + .source = timeline[0].source, + }; + + mpctx->timeline = timeline; + // the last part is not included it in the count + mpctx->num_timeline_parts = track_count + 1 - 1; + mpctx->chapters = chapters; + mpctx->num_chapters = track_count; + +out: + talloc_free(ctx); +} diff --git a/mpvcore/timeline/tl_edl.c b/mpvcore/timeline/tl_edl.c new file mode 100644 index 0000000000..4afb346dae --- /dev/null +++ b/mpvcore/timeline/tl_edl.c @@ -0,0 +1,392 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdlib.h> +#include <stdbool.h> +#include <inttypes.h> +#include <ctype.h> + +#include "talloc.h" + +#include "core/mp_core.h" +#include "core/mp_msg.h" +#include "demux/demux.h" +#include "core/path.h" +#include "core/bstr.h" +#include "core/mp_common.h" +#include "stream/stream.h" + + +struct edl_source { + struct bstr id; + char *filename; + int lineno; +}; + +struct edl_time { + int64_t start; + int64_t end; + bool implied_start; + bool implied_end; +}; + +struct edl_part { + struct edl_time tl; + struct edl_time src; + int64_t duration; + int id; + int lineno; +}; + +static int find_edl_source(struct edl_source *sources, int num_sources, + struct bstr name) +{ + for (int i = 0; i < num_sources; i++) + if (!bstrcmp(sources[i].id, name)) + return i; + return -1; +} + +void build_edl_timeline(struct MPContext *mpctx) +{ + const struct bstr file_prefix = bstr0("<"); + void *tmpmem = talloc_new(NULL); + + struct bstr *lines = bstr_splitlines(tmpmem, mpctx->demuxer->file_contents); + int linec = MP_TALLOC_ELEMS(lines); + struct bstr header = bstr0("mplayer EDL file, version "); + if (!linec || !bstr_startswith(lines[0], header)) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Bad EDL header!\n"); + goto out; + } + struct bstr version = bstr_strip(bstr_cut(lines[0], header.len)); + if (bstrcmp(bstr0("2"), version)) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Unsupported EDL file version!\n"); + goto out; + } + int num_sources = 0; + int num_parts = 0; + for (int i = 1; i < linec; i++) { + if (bstr_startswith(lines[i], file_prefix)) { + num_sources++; + } else { + int comment = bstrchr(lines[i], '#'); + if (comment >= 0) + lines[i] = bstr_splice(lines[i], 0, comment); + if (bstr_strip(lines[i]).len) + num_parts++; + } + } + if (!num_parts) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "No parts in timeline!\n"); + goto out; + } + + // Parse source filename definitions + + struct edl_source *edl_ids = talloc_array_ptrtype(tmpmem, edl_ids, + num_sources); + num_sources = 0; + for (int i = 1; i < linec; i++) { + struct bstr line = lines[i]; + if (!bstr_startswith(line, file_prefix)) + continue; + line = bstr_cut(line, file_prefix.len); + struct bstr id = bstr_split(line, WHITESPACE, &line); + if (find_edl_source(edl_ids, num_sources, id) >= 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Repeated ID on line %d!\n", + i+1); + goto out; + } + if (!isalpha(*id.start)) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Invalid ID on line %d!\n", + i+1); + goto out; + } + char *filename = mp_basename(bstrdup0(tmpmem, bstr_strip(line))); + if (!strlen(filename)) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, + "EDL: Invalid filename on line %d!\n", i+1); + goto out; + } + struct bstr dirname = mp_dirname(mpctx->demuxer->filename); + char *fullname = mp_path_join(tmpmem, dirname, bstr0(filename)); + edl_ids[num_sources++] = (struct edl_source){id, fullname, i+1}; + } + + // Parse timeline part definitions + + struct edl_part *parts = talloc_array_ptrtype(tmpmem, parts, num_parts); + int total_parts = num_parts; + num_parts = 0; + for (int i = 1; i < linec; i++) { + struct bstr line = bstr_strip(lines[i]); + if (!line.len || bstr_startswith(line, file_prefix)) + continue; + parts[num_parts] = (struct edl_part){{-1, -1}, {-1, -1}, 0, -1}; + parts[num_parts].lineno = i + 1; + for (int s = 0; s < 2; s++) { + struct edl_time *p = !s ? &parts[num_parts].tl : + &parts[num_parts].src; + while (1) { + struct bstr t = bstr_split(line, WHITESPACE, &line); + if (!t.len) { + if (!s && num_parts < total_parts - 1) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: missing source " + "identifier on line %d (not last)!\n", i+1); + goto out; + } + break; + } + if (isalpha(*t.start)) { + if (s) + goto bad; + parts[num_parts].id = find_edl_source(edl_ids, num_sources, + t); + if (parts[num_parts].id < 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Undefined source " + "identifier on line %d!\n", i+1); + goto out; + } + break; + } + while (t.len) { + struct bstr next; + struct bstr arg = bstr_split(t, "+-", &next); + if (!arg.len) { + next = bstr_split(line, WHITESPACE, &line); + arg = bstr_split(next, "+-", &next); + } + if (!arg.len) + goto bad; + int64_t val; + if (!bstrcmp(arg, bstr0("*"))) + val = -1; + else if (isdigit(*arg.start)) { + val = bstrtoll(arg, &arg, 10) * 1000000000; + if (arg.len && *arg.start == '.') { + int len = arg.len - 1; + arg = bstr_splice(arg, 1, 10); + int64_t val2 = bstrtoll(arg, &arg, 10); + if (arg.len) + goto bad; + for (; len < 9; len++) + val2 *= 10; + val += val2; + } + } else + goto bad; + int c = *t.start; + if (isdigit(c) || c == '*') { + if (val < 0) + p->implied_start = true; + else + p->start = val; + } else if (c == '-') { + if (val < 0) + p->implied_end = true; + else + p->end = val; + } else if (c == '+') { + if (val < 0) + goto bad; + if (val == 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: zero duration " + "on line %d!\n", i+1); + goto out; + } + parts[num_parts].duration = val; + } else + goto bad; + t = next; + } + } + } + num_parts++; + continue; + bad: + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Malformed line %d!\n", i+1); + goto out; + } + + // Fill in implied start/stop/duration values + + int64_t *times = talloc_zero_array(tmpmem, int64_t, num_sources); + while (1) { + int64_t time = 0; + for (int i = 0; i < num_parts; i++) { + for (int s = 0; s < 2; s++) { + struct edl_time *p = s ? &parts[i].tl : &parts[i].src; + if (!s && parts[i].id == -1) + continue; + int64_t *t = s ? &time : times + parts[i].id; + p->implied_start |= s && *t >= 0; + if (p->implied_start && p->start >= 0 && *t >= 0 + && p->start != *t) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Inconsistent line " + "%d!\n", parts[i].lineno); + goto out; + } + if (p->start >= 0) + *t = p->start; + if (p->implied_start) + p->start = *t; + if (*t >= 0 && parts[i].duration) + *t += parts[i].duration; + else + *t = -1; + if (p->end >= 0) + *t = p->end; + } + } + for (int i = 0; i < num_sources; i++) + times[i] = -1; + time = -1; + for (int i = num_parts - 1; i >= 0; i--) { + for (int s = 0; s < 2; s++) { + struct edl_time *p = s ? &parts[i].tl : &parts[i].src; + if (!s && parts[i].id == -1) + continue; + int64_t *t = s ? &time : times + parts[i].id; + p->implied_end |= s && *t >= 0; + if (p->implied_end && p->end >= 0 && *t >=0 && p->end != *t) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Inconsistent line " + "%d!\n", parts[i].lineno); + goto out; + } + if (p->end >= 0) + *t = p->end; + if (p->implied_end) + p->end = *t; + if (*t >= 0 && parts[i].duration) { + *t -= parts[i].duration; + if (*t < 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Negative time " + "on line %d!\n", parts[i].lineno); + goto out; + } + } else + *t = -1; + if (p->start >= 0) + *t = p->start; + } + } + int missing_duration = -1; + int missing_srcstart = -1; + bool anything_done = false; + for (int i = 0; i < num_parts; i++) { + int64_t duration = parts[i].duration; + if (parts[i].tl.start >= 0 && parts[i].tl.end >= 0) { + int64_t duration2 = parts[i].tl.end - parts[i].tl.start; + if (duration && duration != duration2) + goto incons; + duration = duration2; + if (duration <= 0) + goto neg; + } + if (parts[i].src.start >= 0 && parts[i].src.end >= 0) { + int64_t duration2 = parts[i].src.end - parts[i].src.start; + if (duration && duration != duration2) { + incons: + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Inconsistent line " + "%d!\n", i+1); + goto out; + } + duration = duration2; + if (duration <= 0) { + neg: + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: duration <= 0 on " + "line %d!\n", parts[i].lineno); + goto out; + } + } + if (parts[i].id == -1) + continue; + if (!duration) + missing_duration = i; + else if (!parts[i].duration) + anything_done = true; + parts[i].duration = duration; + if (duration && parts[i].src.start < 0) { + if (parts[i].src.end < 0) + missing_srcstart = i; + else + parts[i].src.start = parts[i].src.end - duration; + } + } + if (!anything_done) { + if (missing_duration >= 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Could not determine " + "duration for line %d!\n", + parts[missing_duration].lineno); + goto out; + } + if (missing_srcstart >= 0) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: no source start time for " + "line %d!\n", parts[missing_srcstart].lineno); + goto out; + } + break; + } + } + + // Open source files + + struct demuxer **sources = talloc_array_ptrtype(NULL, sources, + num_sources + 1); + mpctx->sources = sources; + sources[0] = mpctx->demuxer; + mpctx->num_sources = 1; + + for (int i = 0; i < num_sources; i++) { + struct stream *s = stream_open(edl_ids[i].filename, mpctx->opts); + if (!s) + goto openfail; + struct demuxer *d = demux_open(s, NULL, NULL, mpctx->opts); + if (!d) { + free_stream(s); + openfail: + mp_msg(MSGT_CPLAYER, MSGL_ERR, "EDL: Could not open source " + "file on line %d!\n", edl_ids[i].lineno); + goto out; + } + sources[mpctx->num_sources] = d; + mpctx->num_sources++; + } + + // Write final timeline structure + + struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline, + num_parts + 1); + int64_t starttime = 0; + for (int i = 0; i < num_parts; i++) { + timeline[i].start = starttime / 1e9; + starttime += parts[i].duration; + timeline[i].source_start = parts[i].src.start / 1e9; + timeline[i].source = sources[parts[i].id + 1]; + } + if (parts[num_parts - 1].id != -1) { + timeline[num_parts].start = starttime / 1e9; + num_parts++; + } + mpctx->timeline = timeline; + mpctx->num_timeline_parts = num_parts - 1; + + out: + talloc_free(tmpmem); +} diff --git a/mpvcore/timeline/tl_matroska.c b/mpvcore/timeline/tl_matroska.c new file mode 100644 index 0000000000..cc6e62d429 --- /dev/null +++ b/mpvcore/timeline/tl_matroska.c @@ -0,0 +1,374 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include <stdlib.h> +#include <stdbool.h> +#include <inttypes.h> +#include <assert.h> +#include <dirent.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <unistd.h> +#include <libavutil/common.h> + +#include "osdep/io.h" + +#include "talloc.h" + +#include "core/mp_core.h" +#include "core/mp_msg.h" +#include "demux/demux.h" +#include "core/path.h" +#include "core/bstr.h" +#include "core/mp_common.h" +#include "stream/stream.h" + +struct find_entry { + char *name; + int matchlen; + off_t size; +}; + +static int cmp_entry(const void *pa, const void *pb) +{ + const struct find_entry *a = pa, *b = pb; + // check "similar" filenames first + int matchdiff = b->matchlen - a->matchlen; + if (matchdiff) + return FFSIGN(matchdiff); + // check small files first + off_t sizediff = a->size - b->size; + if (sizediff) + return FFSIGN(sizediff); + return 0; +} + +static char **find_files(const char *original_file, const char *suffix) +{ + void *tmpmem = talloc_new(NULL); + char *basename = mp_basename(original_file); + struct bstr directory = mp_dirname(original_file); + char **results = talloc_size(NULL, 0); + char *dir_zero = bstrdup0(tmpmem, directory); + DIR *dp = opendir(dir_zero); + if (!dp) { + talloc_free(tmpmem); + return results; + } + struct find_entry *entries = NULL; + struct dirent *ep; + int num_results = 0; + while ((ep = readdir(dp))) { + int suffix_offset = strlen(ep->d_name) - strlen(suffix); + // name must end with suffix + if (suffix_offset < 0 || strcmp(ep->d_name + suffix_offset, suffix)) + continue; + // don't list the original name + if (!strcmp(ep->d_name, basename)) + continue; + + char *name = mp_path_join(results, directory, bstr0(ep->d_name)); + char *s1 = ep->d_name; + char *s2 = basename; + int matchlen = 0; + while (*s1 && *s1++ == *s2++) + matchlen++; + // be a bit more fuzzy about matching the filename + matchlen = (matchlen + 3) / 5; + + struct stat statbuf; + if (stat(name, &statbuf) != 0) + continue; + off_t size = statbuf.st_size; + + entries = talloc_realloc(tmpmem, entries, struct find_entry, + num_results + 1); + entries[num_results] = (struct find_entry) { name, matchlen, size }; + num_results++; + } + closedir(dp); + // NOTE: maybe should make it compare pointers instead + if (entries) + qsort(entries, num_results, sizeof(struct find_entry), cmp_entry); + results = talloc_realloc(NULL, results, char *, num_results); + for (int i = 0; i < num_results; i++) { + results[i] = entries[i].name; + } + talloc_free(tmpmem); + return results; +} + +static int enable_cache(struct MPContext *mpctx, struct stream **stream, + struct demuxer **demuxer, struct demuxer_params *params) +{ + struct MPOpts *opts = mpctx->opts; + + if (opts->stream_cache_size <= 0) + return 0; + + char *filename = talloc_strdup(NULL, (*demuxer)->filename); + free_demuxer(*demuxer); + free_stream(*stream); + + *stream = stream_open(filename, opts); + if (!*stream) { + talloc_free(filename); + return -1; + } + + stream_enable_cache_percent(stream, + opts->stream_cache_size, + opts->stream_cache_def_size, + opts->stream_cache_min_percent, + opts->stream_cache_seek_min_percent); + + *demuxer = demux_open(*stream, "mkv", params, opts); + if (!*demuxer) { + talloc_free(filename); + free_stream(*stream); + return -1; + } + + talloc_free(filename); + return 1; +} + +// segment = get Nth segment of a multi-segment file +static bool check_file_seg(struct MPContext *mpctx, struct demuxer **sources, + int num_sources, unsigned char uid_map[][16], + char *filename, int segment) +{ + bool was_valid = false; + struct demuxer_params params = { + .matroska_wanted_uids = uid_map, + .matroska_wanted_segment = segment, + .matroska_was_valid = &was_valid, + }; + struct stream *s = stream_open(filename, mpctx->opts); + if (!s) + return false; + struct demuxer *d = demux_open(s, "mkv", ¶ms, mpctx->opts); + + if (!d) { + free_stream(s); + return was_valid; + } + if (d->type == DEMUXER_TYPE_MATROSKA) { + for (int i = 1; i < num_sources; i++) { + if (sources[i]) + continue; + if (!memcmp(uid_map[i], d->matroska_data.segment_uid, 16)) { + mp_msg(MSGT_CPLAYER, MSGL_INFO, "Match for source %d: %s\n", + i, d->filename); + + if (enable_cache(mpctx, &s, &d, ¶ms) < 0) + continue; + + sources[i] = d; + return true; + } + } + } + free_demuxer(d); + free_stream(s); + return was_valid; +} + +static void check_file(struct MPContext *mpctx, struct demuxer **sources, + int num_sources, unsigned char uid_map[][16], + char *filename, int first) +{ + for (int segment = first; ; segment++) { + if (!check_file_seg(mpctx, sources, num_sources, uid_map, + filename, segment)) + break; + } +} + +static bool missing(struct demuxer **sources, int num_sources) +{ + for (int i = 0; i < num_sources; i++) { + if (!sources[i]) + return true; + } + return false; +} + +static int find_ordered_chapter_sources(struct MPContext *mpctx, + struct demuxer **sources, + int num_sources, + unsigned char uid_map[][16]) +{ + int num_filenames = 0; + char **filenames = NULL; + if (num_sources > 1) { + char *main_filename = mpctx->demuxer->filename; + mp_msg(MSGT_CPLAYER, MSGL_INFO, "This file references data from " + "other sources.\n"); + if (mpctx->demuxer->stream->uncached_type != STREAMTYPE_FILE) { + mp_msg(MSGT_CPLAYER, MSGL_WARN, "Playback source is not a " + "normal disk file. Will not search for related files.\n"); + } else { + mp_msg(MSGT_CPLAYER, MSGL_INFO, "Will scan other files in the " + "same directory to find referenced sources.\n"); + filenames = find_files(main_filename, ".mkv"); + num_filenames = MP_TALLOC_ELEMS(filenames); + } + // Possibly get further segments appended to the first segment + check_file(mpctx, sources, num_sources, uid_map, main_filename, 1); + } + + for (int i = 0; i < num_filenames; i++) { + if (!missing(sources, num_sources)) + break; + mp_msg(MSGT_CPLAYER, MSGL_INFO, "Checking file %s\n", filenames[i]); + check_file(mpctx, sources, num_sources, uid_map, filenames[i], 0); + } + + talloc_free(filenames); + if (missing(sources, num_sources)) { + mp_msg(MSGT_CPLAYER, MSGL_ERR, "Failed to find ordered chapter part!\n" + "There will be parts MISSING from the video!\n"); + int j = 1; + for (int i = 1; i < num_sources; i++) + if (sources[i]) { + sources[j] = sources[i]; + memcpy(uid_map[j], uid_map[i], 16); + j++; + } + num_sources = j; + } + return num_sources; +} + +void build_ordered_chapter_timeline(struct MPContext *mpctx) +{ + struct MPOpts *opts = mpctx->opts; + + if (!opts->ordered_chapters) { + mp_msg(MSGT_CPLAYER, MSGL_INFO, "File uses ordered chapters, but " + "you have disabled support for them. Ignoring.\n"); + return; + } + + mp_msg(MSGT_CPLAYER, MSGL_INFO, "File uses ordered chapters, will build " + "edit timeline.\n"); + + struct demuxer *demuxer = mpctx->demuxer; + struct matroska_data *m = &demuxer->matroska_data; + + // +1 because sources/uid_map[0] is original file even if all chapters + // actually use other sources and need separate entries + struct demuxer **sources = talloc_array_ptrtype(NULL, sources, + m->num_ordered_chapters+1); + sources[0] = mpctx->demuxer; + unsigned char (*uid_map)[16] = talloc_array_ptrtype(NULL, uid_map, + m->num_ordered_chapters + 1); + int num_sources = 1; + memcpy(uid_map[0], m->segment_uid, 16); + + for (int i = 0; i < m->num_ordered_chapters; i++) { + struct matroska_chapter *c = m->ordered_chapters + i; + if (!c->has_segment_uid) + memcpy(c->segment_uid, m->segment_uid, 16); + + for (int j = 0; j < num_sources; j++) + if (!memcmp(c->segment_uid, uid_map[j], 16)) + goto found1; + memcpy(uid_map[num_sources], c->segment_uid, 16); + sources[num_sources] = NULL; + num_sources++; + found1: + ; + } + + num_sources = find_ordered_chapter_sources(mpctx, sources, num_sources, + uid_map); + + + // +1 for terminating chapter with start time marking end of last real one + struct timeline_part *timeline = talloc_array_ptrtype(NULL, timeline, + m->num_ordered_chapters + 1); + struct chapter *chapters = talloc_array_ptrtype(NULL, chapters, + m->num_ordered_chapters); + uint64_t starttime = 0; + uint64_t missing_time = 0; + int part_count = 0; + int num_chapters = 0; + uint64_t prev_part_offset = 0; + for (int i = 0; i < m->num_ordered_chapters; i++) { + struct matroska_chapter *c = m->ordered_chapters + i; + + int j; + for (j = 0; j < num_sources; j++) { + if (!memcmp(c->segment_uid, uid_map[j], 16)) + goto found2; + } + missing_time += c->end - c->start; + continue; + found2:; + /* Only add a separate part if the time or file actually changes. + * Matroska files have chapter divisions that are redundant from + * timeline point of view because the same chapter structure is used + * both to specify the timeline and for normal chapter information. + * Removing a missing inserted external chapter can also cause this. + * We allow for a configurable fudge factor because of files which + * specify chapter end times that are one frame too early; + * we don't want to try seeking over a one frame gap. */ + int64_t join_diff = c->start - starttime - prev_part_offset; + if (part_count == 0 + || FFABS(join_diff) > opts->chapter_merge_threshold * 1000000 + || sources[j] != timeline[part_count - 1].source) { + timeline[part_count].source = sources[j]; + timeline[part_count].start = starttime / 1e9; + timeline[part_count].source_start = c->start / 1e9; + prev_part_offset = c->start - starttime; + part_count++; + } else if (part_count > 0 && join_diff) { + /* Chapter was merged at an inexact boundary; + * adjust timestamps to match. */ + mp_msg(MSGT_CPLAYER, MSGL_V, "Merging timeline part %d with " + "offset %g ms.\n", i, join_diff / 1e6); + starttime += join_diff; + } + chapters[num_chapters].start = starttime / 1e9; + chapters[num_chapters].name = talloc_strdup(chapters, c->name); + starttime += c->end - c->start; + num_chapters++; + } + timeline[part_count].start = starttime / 1e9; + talloc_free(uid_map); + + if (!part_count) { + // None of the parts come from the file itself??? + talloc_free(sources); + talloc_free(timeline); + talloc_free(chapters); + return; + } + + if (missing_time) + mp_msg(MSGT_CPLAYER, MSGL_ERR, "There are %.3f seconds missing " + "from the timeline!\n", missing_time / 1e9); + mpctx->sources = sources; + mpctx->num_sources = num_sources; + mpctx->timeline = timeline; + mpctx->num_timeline_parts = part_count; + mpctx->num_chapters = num_chapters; + mpctx->chapters = chapters; +} diff --git a/mpvcore/version.c b/mpvcore/version.c new file mode 100644 index 0000000000..23a0c59bc3 --- /dev/null +++ b/mpvcore/version.c @@ -0,0 +1,26 @@ +/* + * This file is part of MPlayer. + * + * MPlayer is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * MPlayer 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with MPlayer; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + +#include "version.h" +#ifdef NO_BUILD_TIMESTAMPS +#undef BUILDDATE +#define BUILDDATE "UNKNOWN" +#endif + +const char *mplayer_version = "mpv " VERSION; +const char *mplayer_builddate = BUILDDATE; |