Something went wrong. Try again.
🎥 Command line media player
Something went wrong. Try again.
C
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474/* * This file is part of mpv. * * mpv is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * 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 Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with mpv. If not, see <http://www.gnu.org/licenses/>. */
#include <errno.h>#include <stddef.h>#include <stdbool.h>#include <sys/types.h>#include <sys/stat.h>#include <fcntl.h>
#ifdef _WIN32#include <sys/utime.h>#else#include <utime.h>#endif
#include <libavutil/md5.h>
#include "mpv_talloc.h"
#include "osdep/io.h"
#include "common/encode.h"#include "common/msg.h"#include "misc/ctype.h"#include "options/path.h"#include "options/m_config.h"#include "options/m_config_frontend.h"#include "options/parse_configfile.h"#include "common/playlist.h"#include "options/options.h"#include "options/m_property.h"#include "input/input.h"
#include "stream/stream.h"
#include "core.h"#include "command.h"
static void load_all_cfgfiles(struct MPContext *mpctx, char *section, char *filename){ char **cf = mp_find_all_config_files(NULL, mpctx->global, filename); for (int i = 0; cf && cf[i]; i++) m_config_parse_config_file(mpctx->mconfig, mpctx->global, cf[i], section, 0); talloc_free(cf);}
// This name is used in builtin.conf to force encoding defaults (like ao/vo).#define SECT_ENCODE "encoding"
void mp_parse_cfgfiles(struct MPContext *mpctx){ struct MPOpts *opts = mpctx->opts;
mp_mk_user_dir(mpctx->global, "home", "");
char *p1 = mp_get_user_path(NULL, mpctx->global, "~~home/"); char *p2 = mp_get_user_path(NULL, mpctx->global, "~~old_home/"); if (strcmp(p1, p2) != 0 && mp_path_exists(p2)) { MP_WARN(mpctx, "Warning, two config dirs found:\n %s (main)\n" " %s (bogus)\nYou should merge or delete the second one.\n", p1, p2); } talloc_free(p1); talloc_free(p2);
char *section = NULL; bool encoding = opts->encode_opts->file && opts->encode_opts->file[0]; // In encoding mode, we don't want to apply normal config options. // So we "divert" normal options into a separate section, and the diverted // section is never used - unless maybe it's explicitly referenced from an // encoding profile. if (encoding) section = "playback-default";
load_all_cfgfiles(mpctx, NULL, "encoding-profiles.conf");
load_all_cfgfiles(mpctx, section, "mpv.conf|config");
if (encoding) { m_config_set_profile(mpctx->mconfig, SECT_ENCODE, 0); mp_input_enable_section(mpctx->input, "encode", MP_INPUT_EXCLUSIVE); }}
static int try_load_config(struct MPContext *mpctx, const char *file, int flags, int msgl){ if (!mp_path_exists(file)) return 0; MP_MSG(mpctx, msgl, "Loading config '%s'\n", file); m_config_parse_config_file(mpctx->mconfig, mpctx->global, file, NULL, flags); return 1;}
// Set options file-local, and don't set them if the user set them via the// command line.#define FILE_LOCAL_FLAGS (M_SETOPT_BACKUP | M_SETOPT_PRESERVE_CMDLINE)
static void mp_load_per_file_config(struct MPContext *mpctx){ struct MPOpts *opts = mpctx->opts; char *confpath; char cfg[512]; const char *file = mpctx->filename;
if (opts->use_filedir_conf) { if (snprintf(cfg, sizeof(cfg), "%s.conf", file) >= sizeof(cfg)) { MP_VERBOSE(mpctx, "Filename is too long, can not load file or " "directory specific config files\n"); return; }
char *name = mp_basename(cfg);
bstr dir = mp_dirname(cfg); char *dircfg = mp_path_join_bstr(NULL, dir, bstr0("mpv.conf")); try_load_config(mpctx, dircfg, FILE_LOCAL_FLAGS, MSGL_INFO); talloc_free(dircfg);
if (try_load_config(mpctx, cfg, FILE_LOCAL_FLAGS, MSGL_INFO)) return;
if ((confpath = mp_find_config_file(NULL, mpctx->global, name))) { try_load_config(mpctx, confpath, FILE_LOCAL_FLAGS, MSGL_INFO);
talloc_free(confpath); } }}
static void mp_auto_load_profile(struct MPContext *mpctx, char *category, bstr item){ if (!item.len) return;
char t[512]; snprintf(t, sizeof(t), "%s.%.*s", category, BSTR_P(item)); m_profile_t *p = m_config_get_profile0(mpctx->mconfig, t); if (p) { MP_INFO(mpctx, "Auto-loading profile '%s'\n", t); m_config_set_profile(mpctx->mconfig, t, FILE_LOCAL_FLAGS); }}
void mp_load_auto_profiles(struct MPContext *mpctx){ mp_auto_load_profile(mpctx, "protocol", mp_split_proto(bstr0(mpctx->filename), NULL)); mp_auto_load_profile(mpctx, "extension", bstr0(mp_splitext(mpctx->filename, NULL)));
mp_load_per_file_config(mpctx);}
#define MP_WATCH_LATER_CONF "watch_later"
static bool check_mtime(const char *f1, const char *f2){ struct stat st1, st2; if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0) return false; return st1.st_mtime == st2.st_mtime;}
static bool copy_mtime(const char *f1, const char *f2){ struct stat st1, st2;
if (stat(f1, &st1) != 0 || stat(f2, &st2) != 0) return false;
struct utimbuf ut = { .actime = st2.st_atime, // we want to pass this through intact .modtime = st1.st_mtime, };
if (utime(f2, &ut) != 0) return false;
return true;}
char *mp_get_playback_resume_dir(struct MPContext *mpctx){ char *wl_dir = mpctx->opts->watch_later_dir; if (wl_dir && wl_dir[0]) { wl_dir = mp_get_user_path(mpctx, mpctx->global, wl_dir); } else { wl_dir = mp_find_user_file(mpctx, mpctx->global, "state", MP_WATCH_LATER_CONF); } return wl_dir;}
static char *mp_get_playback_resume_config_filename(struct MPContext *mpctx, const char *fname){ struct MPOpts *opts = mpctx->opts; char *res = NULL; void *tmp = talloc_new(NULL); const char *path = NULL; if (mp_is_url(bstr0(fname))) { path = fname; } else if (opts->ignore_path_in_watch_later_config) { path = mp_basename(fname); } else { path = mp_normalize_path(tmp, fname); if (!path) goto exit; } uint8_t md5[16]; av_md5_sum(md5, path, strlen(path)); char *conf = talloc_strdup(tmp, ""); for (int i = 0; i < 16; i++) conf = talloc_asprintf_append(conf, "%02X", md5[i]);
char *wl_dir = mp_get_playback_resume_dir(mpctx); if (wl_dir && wl_dir[0]) res = mp_path_join(NULL, wl_dir, conf);
exit: talloc_free(tmp); return res;}
// Should follow what parser-cfg.c does/needsstatic bool needs_config_quoting(const char *s){ if (s[0] == '%') return true; for (int i = 0; s[i]; i++) { unsigned char c = s[i]; if (!mp_isprint(c) || mp_isspace(c) || c == '#' || c == '\'' || c == '"') return true; } return false;}
static void write_filename(struct MPContext *mpctx, FILE *file, char *filename){ if (mpctx->opts->ignore_path_in_watch_later_config && !mp_is_url(bstr0(filename))) filename = mp_basename(filename);
if (mpctx->opts->write_filename_in_watch_later_config) { char write_name[1024] = {0}; for (int n = 0; filename[n] && n < sizeof(write_name) - 1; n++) write_name[n] = (unsigned char)filename[n] < 32 ? '_' : filename[n]; fprintf(file, "# %s\n", write_name); }}
static void write_redirect(struct MPContext *mpctx, char *path){ char *conffile = mp_get_playback_resume_config_filename(mpctx, path); if (conffile) { FILE *file = fopen(conffile, "wb"); if (file) { fprintf(file, "# redirect entry\n"); write_filename(mpctx, file, path); fclose(file); }
if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(path)) && !copy_mtime(path, conffile)) MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", path, conffile);
talloc_free(conffile); }}
static void write_redirects_for_parent_dirs(struct MPContext *mpctx, char *path){ if (mp_is_url(bstr0(path)) || mpctx->opts->ignore_path_in_watch_later_config) return;
// Write redirect entries for the file's parent directories to allow // resuming playback when playing parent directories whose entries are // expanded only the first time they are "played". For example, if // "/a/b/c.mkv" is the current entry, also create resume files for /a/b and // /a, so that "mpv --directory-mode=lazy /a" resumes playback from // /a/b/c.mkv even when b isn't the first directory in /a. bstr dir = mp_dirname(path); // There is no need to write a redirect entry for "/". while (dir.len > 1 && dir.len < strlen(path)) { path[dir.len] = '\0'; mp_path_strip_trailing_separator(path); write_redirect(mpctx, path); dir = mp_dirname(path); }}
void mp_write_watch_later_conf(struct MPContext *mpctx){ struct playlist_entry *cur = mpctx->playing; char *conffile = NULL; void *ctx = talloc_new(NULL);
if (!cur) goto exit;
char *path = mp_normalize_path(ctx, cur->filename); if (!path) goto exit;
struct demuxer *demux = mpctx->demuxer;
conffile = mp_get_playback_resume_config_filename(mpctx, path); if (!conffile) goto exit;
char *wl_dir = mp_get_playback_resume_dir(mpctx); mp_mkdirp(wl_dir);
MP_INFO(mpctx, "Saving state.\n");
FILE *file = fopen(conffile, "wb"); if (!file) { MP_WARN(mpctx, "Can't open %s for writing\n", conffile); goto exit; }
write_filename(mpctx, file, path);
bool write_start = true; double pos = get_playback_time(mpctx);
if ((demux && (!demux->seekable || demux->partially_seekable)) || pos == MP_NOPTS_VALUE) { write_start = false; MP_INFO(mpctx, "Not seekable, or time unknown - not saving position.\n"); } char **watch_later_options = mpctx->opts->watch_later_options; for (int i = 0; watch_later_options && watch_later_options[i]; i++) { char *pname = watch_later_options[i]; // Always save start if we have it in the array. if (write_start && strcmp(pname, "start") == 0) { fprintf(file, "%s=%f\n", pname, pos); continue; } // Only store it if it's different from the initial value. if (m_config_watch_later_backup_opt_changed(mpctx->mconfig, pname)) { char *val = NULL; mp_property_do(pname, M_PROPERTY_GET_STRING, &val, mpctx); if (needs_config_quoting(val)) { // e.g. '%6%STRING' fprintf(file, "%s=%%%d%%%s\n", pname, (int)strlen(val), val); } else { fprintf(file, "%s=%s\n", pname, val); } talloc_free(val); } } fclose(file);
if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(path)) && !copy_mtime(path, conffile)) { MP_WARN(mpctx, "Can't copy mtime from %s to %s\n", cur->filename, conffile); }
write_redirects_for_parent_dirs(mpctx, path);
// Also write redirect entries for a playlist that mpv expanded if the // current entry is a URL, this is mostly useful for playing multiple // archives of images, e.g. with mpv 1.zip 2.zip and quit-watch-later // on 2.zip, write redirect entries for 2.zip, not just for the archive:// // URL. if (cur->playlist_path && mp_is_url(bstr0(path))) { char *playlist_path = mp_normalize_path(ctx, cur->playlist_path); write_redirect(mpctx, playlist_path); write_redirects_for_parent_dirs(mpctx, playlist_path); }
exit: talloc_free(conffile); talloc_free(ctx);}
void mp_delete_watch_later_conf(struct MPContext *mpctx, const char *file){ char *path = mp_normalize_path(NULL, file ? file : mpctx->filename); if (!path) goto exit;
char *fname = mp_get_playback_resume_config_filename(mpctx, path); if (!fname) goto exit;
unlink(fname); talloc_free(fname);
if (mp_is_url(bstr0(path)) || mpctx->opts->ignore_path_in_watch_later_config) goto exit;
bstr dir = mp_dirname(path); while (dir.len > 1 && dir.len < strlen(path)) { path[dir.len] = '\0'; mp_path_strip_trailing_separator(path); fname = mp_get_playback_resume_config_filename(mpctx, path); if (!fname) break; unlink(fname); talloc_free(fname); dir = mp_dirname(path); }
exit: talloc_free(path);}
bool mp_load_playback_resume(struct MPContext *mpctx, const char *file){ bool resume = false; if (!mpctx->opts->position_resume) return resume; char *fname = mp_get_playback_resume_config_filename(mpctx, file); if (fname && mp_path_exists(fname)) { if (mpctx->opts->position_check_mtime && !mp_is_url(bstr0(file)) && !check_mtime(file, fname)) { talloc_free(fname); return resume; }
// Never apply the saved start position to following files m_config_backup_opt(mpctx->mconfig, "start"); MP_INFO(mpctx, "Resuming playback. This behavior can " "be disabled with --no-resume-playback.\n"); try_load_config(mpctx, fname, M_SETOPT_PRESERVE_CMDLINE, MSGL_V); resume = true; } talloc_free(fname); return resume;}
// Returns the first file that has a resume config.// Compared to hashing the playlist file or contents and managing separate// resume file for them, this is simpler, and also has the nice property// that appending to a playlist doesn't interfere with resuming (especially// if the playlist comes from the command line).struct playlist_entry *mp_check_playlist_resume(struct MPContext *mpctx, struct playlist *playlist){ if (!mpctx->opts->position_resume) return NULL; for (int n = 0; n < playlist->num_entries; n++) { struct playlist_entry *e = playlist->entries[n]; char *conf = mp_get_playback_resume_config_filename(mpctx, e->filename); bool exists = conf && mp_path_exists(conf); talloc_free(conf); if (exists) return e; } return NULL;}