ffplay: list directories and play files chosen from the list

The o key lists the directory of the input, local or remote, in the
list. Enter or a click lists the chosen directory or plays the chosen
file instead of the input once it opened, so a file that is no media
leaves the input playing. Directories come first, hidden entries are
left out, and the popup of a file shows its size.

Assisted-by: Claude
This commit is contained in:
Michael Niedermayer authored and michaelni committed 2026-10-18 03:19:28 +00:00
1 parent 6601ca68f7
commit 4c838cbfa1
8 files changed
+552 -21

No files matched your search

Vendored
+2
View File
@@ -2544,6 +2544,7 @@ SYSTEM_FUNCS="
pthread_set_name_np
pthread_setaffinity_np
pthread_setname_np
realpath
sched_getaffinity
SecItemImport
SetConsoleTextAttribute
@@ -6911,6 +6912,7 @@ check_func_headers mach/mach_time.h mach_absolute_time
check_func_headers setjmp.h siglongjmp
check_func_headers signal.h sigaction
check_func_headers stdlib.h getenv
check_func_headers stdlib.h realpath
check_func_headers sys/ioctl.h ioctl
check_func_headers sys/stat.h lstat
check_func_headers sys/auxv.h getauxval
+5
View File
@@ -274,6 +274,11 @@ in builds without libass and with the Vulkan renderer.
Keep the chapter list on screen. While it is kept, typing filters the list by
artist and title, down/up move its selection and escape closes it.
@item o
Show the files in the directory of the input. Enter or a click plays the
selected file or opens the selected directory, escape closes the list. Typing
filters the list by name.
@item enter
Seek to the chapter selected in the chapter list while it is shown.
+1
View File
@@ -53,6 +53,7 @@ OBJS-ffprobe += \
OBJS-ffmpeg += $(COMPAT_OBJS:%=compat/%)
OBJS-ffplay += fftools/ffplay_renderer.o
OBJS-ffplay += fftools/ffplay_dir.o
OBJS-ffplay += fftools/ffplay_list.o
define DOFFTOOL
+272 -15
View File
@@ -58,6 +58,7 @@
#include <SDL_thread.h>
#include "cmdutils.h"
#include "ffplay_dir.h"
#include "ffplay_list.h"
#include "ffplay_renderer.h"
#include "opt_common.h"
@@ -312,6 +313,7 @@ typedef struct VideoState {
char *filename;
int id;
SDL_atomic_t replacement;
char *title;
int seek_by_bytes;
int infinite_buffer;
@@ -375,6 +377,23 @@ static int cursor_hidden = 0;
static ListView *list;
/* the key that pins the list when it is released, 0 for none */
static SDL_Keycode list_key;
/* whether the list shows the files of dir_list rather than the chapters of the stream playing */
static int list_shows_files;
static DirList *dir_list;
/* the directory dir_thread reads, taken over by the event loop once done is set */
static struct {
char *path;
AVDictionary *options;
int play_if_no_dir;
int cancelled;
SDL_atomic_t abort;
DirList *dl;
int ret;
SDL_atomic_t done;
} dir_request;
static SDL_Thread *dir_thread;
/* the stream chosen in the file list, paused until it can play and replace the one playing */
static VideoState *next_stream;
static const char **vfilters_list = NULL;
static int nb_vfilters = 0;
static char *afilters = NULL;
@@ -390,6 +409,8 @@ static const char *hwaccel = NULL;
static int is_full_screen;
#define FF_QUIT_EVENT (SDL_USEREVENT + 2)
/* a stream has a picture to show, has no video, no picture before the end of its video, or no room to buffer more */
#define FF_READY_EVENT (SDL_USEREVENT + 3)
static int last_stream_id;
@@ -1040,6 +1061,7 @@ static void draw_video_background(VideoState *is)
}
static int current_chapter(VideoState *is);
static void file_list_listed(VideoState *is);
static void seek_chapter(VideoState *is, int i);
static const char *chapter_tag(const AVChapter *chapter, const char *key)
@@ -1064,9 +1086,12 @@ static int chapter_list_show(VideoState *is, int chapter)
if (ch->end != AV_NOPTS_VALUE && ch->end > ch->start && (uint64_t)ch->end - ch->start <= INT64_MAX)
entries[i].length = av_rescale_q(ch->end - ch->start, ch->time_base, AV_TIME_BASE_Q);
}
list_view_set_playing(list, current_chapter(is));
if (list_shows_files)
list_view_clear(list);
ret = list_view_show(list, entries, is->ic->nb_chapters, chapter);
av_free(entries);
if (ret >= 0)
list_shows_files = 0;
return ret;
}
@@ -1363,6 +1388,7 @@ static void stream_component_close(VideoState *is, int stream_index)
switch (codecpar->codec_type) {
case AVMEDIA_TYPE_AUDIO:
decoder_abort(&is->auddec, &is->sampq);
avfilter_graph_free(&is->agraph);
SDL_CloseAudioDevice(is->audio_dev);
is->audio_dev = 0;
decoder_destroy(&is->auddec);
@@ -1463,6 +1489,21 @@ static void do_exit(VideoState *is)
if (is) {
stream_close(is);
}
if (next_stream)
stream_close(next_stream);
SDL_AtomicSet(&dir_request.abort, 1);
for (int waited = 0; dir_thread && !SDL_AtomicGet(&dir_request.done) && waited < 2000; waited += 10)
SDL_Delay(10);
if (dir_thread && !SDL_AtomicGet(&dir_request.done)) {
/* a protocol that does not take the interrupt keeps the thread and its request until the process ends */
SDL_DetachThread(dir_thread);
} else {
if (dir_thread)
SDL_WaitThread(dir_thread, NULL);
dir_list_free(&dir_request.dl);
av_freep(&dir_request.path);
av_dict_free(&dir_request.options);
}
list_view_free(&list);
if (renderer)
SDL_DestroyRenderer(renderer);
@@ -1478,6 +1519,7 @@ static void do_exit(VideoState *is)
av_freep(&audio_codec_name);
av_freep(&subtitle_codec_name);
av_freep(&input_filename);
dir_list_free(&dir_list);
avformat_network_deinit();
if (show_status)
printf("\n");
@@ -1537,7 +1579,7 @@ static void video_display(VideoState *is)
SDL_RenderClear(renderer);
if (is->audio_st && is->show_mode != SHOW_MODE_VIDEO)
video_audio_display(is);
else if (is->video_st)
else if (is->video_st && is->pictq.rindex_shown)
video_image_display(is);
list_view_draw(list);
SDL_RenderPresent(renderer);
@@ -1857,7 +1899,7 @@ retry:
}
display:
/* display picture */
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO && is->pictq.rindex_shown)
if (!display_disable && is->force_refresh && is->show_mode == SHOW_MODE_VIDEO)
video_display(is);
}
is->force_refresh = 0;
@@ -2344,7 +2386,7 @@ static int video_thread(void *arg)
AVFrame *frame = av_frame_alloc();
double pts;
double duration;
int ret;
int ret, announced = 0;
AVRational tb = is->video_st->time_base;
AVRational frame_rate = av_guess_frame_rate(is->ic, is->video_st, NULL);
@@ -2363,8 +2405,13 @@ static int video_thread(void *arg)
ret = get_video_frame(is, frame);
if (ret < 0)
goto the_end;
if (!ret)
if (!ret) {
if (is->viddec.finished == is->viddec.pkt_serial && !announced) {
push_stream_event(is, FF_READY_EVENT);
announced = 1;
}
continue;
}
if ( last_w != frame->width
|| last_h != frame->height
@@ -2425,6 +2472,10 @@ static int video_thread(void *arg)
pts = (frame->pts == AV_NOPTS_VALUE) ? NAN : frame->pts * av_q2d(tb);
ret = queue_picture(is, frame, pts, duration, fd ? fd->pkt_pos : -1, is->viddec.pkt_serial);
av_frame_unref(frame);
if (ret >= 0 && !announced) {
push_stream_event(is, FF_READY_EVENT);
announced = 1;
}
if (is->videoq.serial != is->viddec.pkt_serial)
break;
}
@@ -2433,6 +2484,8 @@ static int video_thread(void *arg)
goto the_end;
}
the_end:
if (!announced && SDL_AtomicGet(&is->replacement))
push_stream_event(is, FF_QUIT_EVENT);
avfilter_graph_free(&graph);
av_frame_free(&frame);
return 0;
@@ -3012,7 +3065,7 @@ static int stream_component_open(VideoState *is, int stream_index)
is->auddec.start_pts = is->audio_st->start_time;
is->auddec.start_pts_tb = is->audio_st->time_base;
}
if ((ret = audio_start(is)) < 0)
if (!SDL_AtomicGet(&is->replacement) && (ret = audio_start(is)) < 0)
stream_component_close(is, stream_index);
break;
case AVMEDIA_TYPE_VIDEO:
@@ -3150,6 +3203,7 @@ static int read_thread(void *arg)
int64_t stream_start_time;
char metadata_description[96];
int pkt_in_play_range = 0;
int announced = 0;
const AVDictionaryEntry *t;
SDL_mutex *wait_mutex = SDL_CreateMutex();
AVDictionary *format_options = NULL;
@@ -3389,6 +3443,8 @@ static int read_thread(void *arg)
ret = -1;
goto fail;
}
if (is->video_stream < 0)
push_stream_event(is, FF_READY_EVENT);
if (is->infinite_buffer < 0 && is->realtime)
is->infinite_buffer = 1;
@@ -3465,6 +3521,10 @@ static int read_thread(void *arg)
|| (stream_has_enough_packets(is->audio_st, is->audio_stream, &is->audioq) &&
stream_has_enough_packets(is->video_st, is->video_stream, &is->videoq) &&
stream_has_enough_packets(is->subtitle_st, is->subtitle_stream, &is->subtitleq)))) {
if (!announced && SDL_AtomicGet(&is->replacement) && is->audioq.size + is->videoq.size + is->subtitleq.size > MAX_QUEUE_SIZE) {
push_stream_event(is, FF_READY_EVENT);
announced = 1;
}
/* wait 10 ms */
SDL_LockMutex(wait_mutex);
SDL_CondWaitTimeout(is->continue_read_thread, wait_mutex, 10);
@@ -3579,8 +3639,10 @@ static int read_thread(void *arg)
return 0;
}
/* replaced is the stream playing which the new one is to replace, NULL for none; the replacement starts paused with
* the volume of the stream it replaces, and opens no audio device before stream_replace() */
static VideoState *stream_open(const char *filename,
const AVInputFormat *iformat)
const AVInputFormat *iformat, const VideoState *replaced)
{
VideoState *is;
@@ -3639,6 +3701,12 @@ static VideoState *stream_open(const char *filename,
}
is->audio_volume = SDL_MIX_MAXVOLUME * av_clip(startup_volume, 0, 100) / 100;
is->muted = 0;
if (replaced) {
SDL_AtomicSet(&is->replacement, 1);
is->audio_volume = replaced->audio_volume;
is->muted = replaced->muted;
is->paused = is->audclk.paused = is->vidclk.paused = is->extclk.paused = 1;
}
is->av_sync_type = av_sync_type;
is->read_tid = SDL_CreateThread(read_thread, "read_thread", is);
if (!is->read_tid) {
@@ -3767,7 +3835,9 @@ static void refresh_loop_wait_event(VideoState *is, SDL_Event *event) {
}
remaining_time = REFRESH_RATE;
if (list_view_visible(list))
list_view_set_playing(list, current_chapter(is));
list_view_set_playing(list, list_shows_files ? -1 : current_chapter(is));
if (dir_thread && SDL_AtomicGet(&dir_request.done))
file_list_listed(is);
if (list_view_update(list))
is->force_refresh = 1;
if (is->show_mode != SHOW_MODE_NONE && (!is->paused || is->force_refresh))
@@ -3802,6 +3872,171 @@ static void seek_chapter(VideoState *is, int i)
AV_TIME_BASE_Q), 0, 0);
}
/* shows the files of the directory with the entry selected */
static int file_list_show(DirList *dl, int selected)
{
int ret;
if (!list_shows_files)
list_view_clear(list);
ret = list_view_show(list, dl->entries, dl->nb_entries, selected);
if (ret >= 0)
list_shows_files = 1;
return ret;
}
/* opens the file to replace the stream playing once it can play */
static void file_play(VideoState *is, const char *path)
{
if (next_stream)
stream_close(next_stream);
if (!(next_stream = stream_open(path, file_iformat, is)))
av_log(NULL, AV_LOG_ERROR, "Could not open '%s'\n", path);
}
static int dir_interrupt_cb(void *ctx)
{
return SDL_AtomicGet(&dir_request.abort);
}
static int dir_thread_func(void *arg)
{
const AVIOInterruptCB int_cb = { dir_interrupt_cb };
dir_request.ret = dir_list_read(&dir_request.dl, dir_request.path, &int_cb, &dir_request.options);
SDL_AtomicSet(&dir_request.done, 1);
return 0;
}
/* starts reading the directory on dir_thread, unless it is busy; with play_if_no_dir the path is played when it is no
* directory */
static void file_list_read(const char *path, int play_if_no_dir)
{
int ret = 0;
if (dir_thread)
return;
dir_request.play_if_no_dir = play_if_no_dir;
dir_request.cancelled = 0;
SDL_AtomicSet(&dir_request.abort, 0);
SDL_AtomicSet(&dir_request.done, 0);
if (!(dir_request.path = av_strdup(path)))
ret = AVERROR(ENOMEM);
else
ret = av_dict_copy(&dir_request.options, format_opts, 0);
if (ret < 0 || !(dir_thread = SDL_CreateThread(dir_thread_func, "dir_reader", NULL))) {
av_log(NULL, AV_LOG_ERROR, "Could not list the directory '%s': %s\n", path, ret < 0 ? av_err2str(ret) : SDL_GetError());
av_freep(&dir_request.path);
av_dict_free(&dir_request.options);
}
}
/* takes over what dir_thread read: shows the directory, plays the path when it is no directory, or drops the result of
* a cancelled request */
static void file_list_listed(VideoState *is)
{
int ret;
SDL_WaitThread(dir_thread, NULL);
dir_thread = NULL;
av_dict_free(&dir_request.options);
if (dir_request.cancelled) {
dir_list_free(&dir_request.dl);
} else if (dir_request.ret < 0) {
av_log(NULL, dir_request.play_if_no_dir ? AV_LOG_VERBOSE : AV_LOG_ERROR, "Could not list the directory '%s': %s\n",
dir_request.path, av_err2str(dir_request.ret));
if (dir_request.play_if_no_dir)
file_play(is, dir_request.path);
} else if ((ret = file_list_show(dir_request.dl, 0)) < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not show the list: %s\n", av_err2str(ret));
dir_list_free(&dir_request.dl);
} else {
dir_list_free(&dir_list);
dir_list = dir_request.dl;
dir_request.dl = NULL;
list_view_clear_search(list);
list_view_pin(list, 1);
}
av_freep(&dir_request.path);
}
/* pins the file list, showing the directory listed last or else the directory of the input */
static void file_list_open(VideoState *is)
{
const char *name = is->filename, *dir;
char *path;
if (dir_list) {
if (file_list_show(dir_list, -1) >= 0)
list_view_pin(list, 1);
return;
}
av_strstart(name, "file:", &name);
if (!(path = av_strdup(name)))
return;
dir = av_dirname(path);
file_list_read(*dir ? dir : "/", 0);
av_free(path);
}
/* lists the chosen directory, or plays the chosen file */
static void file_list_choose(VideoState *is, int entry)
{
char *path = dir_list_path(dir_list, entry);
if (!path)
return;
if (entry < dir_list->nb_dirs + dir_list->nb_maybe_dirs)
file_list_read(path, entry >= dir_list->nb_dirs);
else
file_play(is, path);
av_free(path);
}
static void list_choose(VideoState *is, int entry)
{
if (list_shows_files)
file_list_choose(is, entry);
else
seek_chapter(is, entry);
}
/* plays next_stream instead of the stream playing, which goes on from its position when next_stream has no video and
* its audio cannot play */
static VideoState *stream_replace(VideoState *is)
{
int audio_stream = is->audio_stream;
double pos = get_master_clock(is);
stream_component_close(is, audio_stream);
SDL_AtomicSet(&next_stream->replacement, 0);
if (next_stream->audio_stream >= 0 && audio_start(next_stream) < 0) {
stream_component_close(next_stream, next_stream->audio_stream);
if (next_stream->video_stream < 0) {
av_log(NULL, AV_LOG_ERROR, "Could not play '%s'\n", next_stream->filename);
stream_close(next_stream);
next_stream = NULL;
if (audio_stream >= 0 && stream_component_open(is, audio_stream) < 0)
av_log(NULL, AV_LOG_ERROR, "Could not reopen the audio of '%s'\n", is->filename);
if (!is->seek_by_bytes && !isnan(pos))
stream_seek(is, (int64_t)(pos * AV_TIME_BASE), 0, 0);
return is;
}
}
/* the chapter entries belong to the stream closed */
if (!list_shows_files)
list_view_clear(list);
list_view_pin(list, 0);
next_stream->width = is->width;
next_stream->height = is->height;
stream_close(is);
is = next_stream;
next_stream = NULL;
SDL_SetWindowTitle(window, window_title ? window_title : is->title ? is->title : is->filename);
toggle_pause(is);
return is;
}
/* handle an event sent by the GUI */
static void event_loop(VideoState *cur_stream)
{
@@ -3823,10 +4058,15 @@ static void event_loop(VideoState *cur_stream)
if (!cur_stream->width)
continue;
if (list_view_pinned(list)) {
if (event.key.keysym.sym == SDLK_PAGEUP || event.key.keysym.sym == SDLK_PAGEDOWN)
if (!list_shows_files && (event.key.keysym.sym == SDLK_PAGEUP || event.key.keysym.sym == SDLK_PAGEDOWN))
seek_chapter(cur_stream, current_chapter(cur_stream) + (event.key.keysym.sym == SDLK_PAGEUP ? 1 : -1));
else if ((chosen = list_view_key(list, event.key.keysym.sym)) >= 0)
seek_chapter(cur_stream, chosen);
list_choose(cur_stream, chosen);
/* escape while a directory is being read for the list */
if (dir_thread && !list_view_pinned(list)) {
dir_request.cancelled = 1;
SDL_AtomicSet(&dir_request.abort, 1);
}
break;
}
switch (event.key.keysym.sym) {
@@ -3869,10 +4109,13 @@ static void event_loop(VideoState *cur_stream)
case SDLK_l:
list_key = cur_stream->ic->nb_chapters > 0 ? SDLK_l : 0;
break;
case SDLK_o:
list_key = SDLK_o;
break;
case SDLK_RETURN:
case SDLK_KP_ENTER:
if (list_view_visible(list) && (chosen = list_view_selected(list)) >= 0)
seek_chapter(cur_stream, chosen);
list_choose(cur_stream, chosen);
break;
case SDLK_w:
if (cur_stream->show_mode == SHOW_MODE_VIDEO && cur_stream->vfilter_idx < nb_vfilters - 1) {
@@ -3939,8 +4182,12 @@ static void event_loop(VideoState *cur_stream)
break;
case SDL_KEYUP:
/* on the release of a press made while unpinned, so that the key's own text input does not start the search */
if (list_key && event.key.keysym.sym == list_key)
chapter_list_pin(cur_stream);
if (list_key && event.key.keysym.sym == list_key) {
if (list_key == SDLK_o)
file_list_open(cur_stream);
else
chapter_list_pin(cur_stream);
}
list_key = 0;
break;
case SDL_TEXTINPUT:
@@ -3959,7 +4206,7 @@ static void event_loop(VideoState *cur_stream)
if (event.button.button == SDL_BUTTON_LEFT && list_view_visible(list) &&
list_view_click(list, event.button.x, event.button.y, &chosen)) {
if (chosen >= 0)
seek_chapter(cur_stream, chosen);
list_choose(cur_stream, chosen);
break;
}
if (event.button.button == SDL_BUTTON_LEFT) {
@@ -4035,7 +4282,16 @@ static void event_loop(VideoState *cur_stream)
cur_stream->force_refresh = 1;
}
break;
case FF_READY_EVENT:
if (next_stream && event.user.code == next_stream->id)
cur_stream = stream_replace(cur_stream);
break;
case FF_QUIT_EVENT:
if (next_stream && event.user.code == next_stream->id) {
av_log(NULL, AV_LOG_ERROR, "Could not play '%s'\n", next_stream->filename);
stream_close(next_stream);
next_stream = NULL;
}
if (event.user.code != cur_stream->id)
break;
av_fallthrough;
@@ -4246,6 +4502,7 @@ void show_help_default(const char *opt, const char *arg)
"down/up seek backward/forward 1 minute\n"
"page down/page up seek to previous/next chapter or backward/forward 10 minutes if no chapters\n"
"l keep the chapter list on screen; typing then filters it, down/up move its selection and escape closes it\n"
"o list the directory of the input; enter or a click opens a directory or plays a file, escape closes the list\n"
"enter seek to the chapter selected in the chapter list while it is shown\n"
"mouse wheel move the chapter list selection while it is shown\n"
"left click seek to the clicked chapter list entry, or sort the list by the clicked column, again to flip it;\n"
@@ -4387,7 +4644,7 @@ int main(int argc, char **argv)
av_log(NULL, AV_LOG_FATAL, "Could not allocate the list\n");
do_exit(NULL);
}
is = stream_open(input_filename, file_iformat);
is = stream_open(input_filename, file_iformat, NULL);
if (!is) {
av_log(NULL, AV_LOG_FATAL, "Failed to initialize VideoState!\n");
do_exit(NULL);
+195
View File
@@ -0,0 +1,195 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#include "config.h"
#include <errno.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#if HAVE_LSTAT
#include <sys/stat.h>
#endif
#include "libavformat/avio.h"
#include "libavutil/avstring.h"
#include "libavutil/dict.h"
#include "libavutil/dynarray.h"
#include "libavutil/error.h"
#include "libavutil/mem.h"
#include "ffplay_dir.h"
enum EntryKind {
ENTRY_FILE,
ENTRY_MAYBE_DIR,
ENTRY_DIR,
};
/* the directories first, then the entries that may be directories, then the files, each by name; the kind of an entry
* is kept in its length until the entries are sorted */
static int compare_entries(const void *a, const void *b)
{
const ListEntry *ea = a, *eb = b;
int kind = (eb->length > ea->length) - (eb->length < ea->length);
int cmp = av_strcasecmp(ea->title, eb->title);
return kind ? kind : cmp ? cmp : strcmp(ea->title, eb->title);
}
/* adds the entry, titled with a trailing slash when it is a directory and with its size when that is not negative */
static int add_entry(DirList *dl, const char *name, enum EntryKind kind, int64_t size)
{
ListEntry *entry;
FF_DYNARRAY_ADD(INT_MAX, sizeof(*entry), dl->entries, dl->nb_entries, {
entry = &dl->entries[dl->nb_entries];
}, {
return AVERROR(ENOMEM);
});
*entry = (ListEntry){ av_asprintf("%s%s", name, kind == ENTRY_DIR ? "/" : ""), NULL, kind, NULL };
if (!entry->title || (size >= 0 && av_dict_set_int(&entry->metadata, "size", size, 0) < 0))
return AVERROR(ENOMEM);
return 0;
}
/* whether the entry is listed, which hidden entries, special files and links to them are not; a link in a local
* directory is listed as what it points to, with its size */
static int listed(const char *local_dir, AVIODirEntry *dirent)
{
if (!dirent->name || !*dirent->name || dirent->name[0] == '.')
return 0;
#if HAVE_LSTAT
if (dirent->type == AVIO_ENTRY_SYMBOLIC_LINK && local_dir) {
char *path = av_append_path_component(local_dir, dirent->name);
struct stat st;
int found;
if (!path)
return AVERROR(ENOMEM);
found = !stat(path, &st);
av_free(path);
if (!found || !(S_ISDIR(st.st_mode) || S_ISREG(st.st_mode)))
return 0;
dirent->type = S_ISDIR(st.st_mode) ? AVIO_ENTRY_DIRECTORY : AVIO_ENTRY_FILE;
dirent->size = st.st_size;
}
#endif
return dirent->type != AVIO_ENTRY_NAMED_PIPE && dirent->type != AVIO_ENTRY_CHARACTER_DEVICE &&
dirent->type != AVIO_ENTRY_BLOCK_DEVICE && dirent->type != AVIO_ENTRY_SOCKET;
}
/* the local directory as an absolute path without links, NULL with errno set when it cannot be resolved */
static char *resolve_local(const char *path)
{
#if HAVE_REALPATH
char *resolved = realpath(path, NULL), *copy = resolved ? av_strdup(resolved) : NULL;
if (resolved && !copy)
errno = ENOMEM;
free(resolved);
return copy;
#else
errno = ENOSYS;
return NULL;
#endif
}
int dir_list_read(DirList **pdl, const char *path, const AVIOInterruptCB *int_cb, AVDictionary **options)
{
const char *protocol = avio_find_protocol_name(path);
int local = !protocol || !strcmp(protocol, "file");
AVIODirContext *ctx = NULL;
AVIODirEntry *dirent = NULL;
DirList *dl = av_mallocz(sizeof(*dl));
int ret;
if (!dl)
return AVERROR(ENOMEM);
if (local)
av_strstart(path, "file:", &path);
dl->path = local ? resolve_local(path) : av_strdup(path);
if (!dl->path) {
ret = AVERROR(local ? errno : ENOMEM);
goto fail;
}
if ((ret = add_entry(dl, "..", ENTRY_DIR, -1)) < 0 || (ret = avio_open_dir2(&ctx, dl->path, int_cb, options)) < 0)
goto fail;
while ((ret = avio_read_dir(ctx, &dirent)) >= 0 && dirent) {
if (int_cb && int_cb->callback && int_cb->callback(int_cb->opaque))
ret = AVERROR_EXIT;
else if ((ret = listed(local ? dl->path : NULL, dirent)) > 0)
ret = add_entry(dl, dirent->name, dirent->type == AVIO_ENTRY_DIRECTORY ? ENTRY_DIR :
dirent->type == AVIO_ENTRY_FILE ? ENTRY_FILE : ENTRY_MAYBE_DIR, dirent->size);
avio_free_directory_entry(&dirent);
if (ret < 0)
break;
}
avio_close_dir(&ctx);
if (ret < 0)
goto fail;
qsort(dl->entries + 1, dl->nb_entries - 1, sizeof(*dl->entries), compare_entries);
for (int i = 0; i < dl->nb_entries; i++) {
dl->nb_dirs += dl->entries[i].length == ENTRY_DIR;
dl->nb_maybe_dirs += dl->entries[i].length == ENTRY_MAYBE_DIR;
dl->entries[i].length = -1;
}
*pdl = dl;
return 0;
fail:
dir_list_free(&dl);
return ret;
}
void dir_list_free(DirList **pdl)
{
DirList *dl = *pdl;
if (!dl)
return;
for (int i = 0; i < dl->nb_entries; i++) {
av_freep(&dl->entries[i].title);
av_dict_free(&dl->entries[i].metadata);
}
av_freep(&dl->entries);
av_freep(&dl->path);
av_freep(pdl);
}
/* the directory above, which is the root of a path or the host of a URL at most */
static char *parent_path(const char *path)
{
const char *scheme = *path == '/' ? NULL : strstr(path, "://");
const char *top = scheme ? strchr(scheme + 3, '/') : path;
const char *slash = top ? strrchr(top, '/') : NULL;
return av_strndup(path, slash ? FFMAX(slash, top + !scheme) - path : strlen(path));
}
char *dir_list_path(const DirList *dl, int entry)
{
const char *title = dl->entries[entry].title;
char *path;
if (!entry)
return parent_path(dl->path);
path = av_append_path_component(dl->path, title);
if (path && entry < dl->nb_dirs)
path[strlen(path) - 1] = 0;
return path;
}
+56
View File
@@ -0,0 +1,56 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file
* The content of a directory as list entries, leaving out hidden entries, special files and links to them.
*/
#ifndef FFTOOLS_FFPLAY_DIR_H
#define FFTOOLS_FFPLAY_DIR_H
#include "ffplay_list.h"
typedef struct DirList {
char *path; ///< the directory as an absolute path, or its URL
ListEntry *entries; ///< ".." first, then the subdirectories and then the files, each by name; directories end with a slash
int nb_entries;
int nb_dirs; ///< the entries up to this one are directories
int nb_maybe_dirs; ///< the entries after the directories up to this one may list as one, without a slash
} DirList;
/**
* Reads the directory into a new list.
*
* @param dl set to the list
* @param path a local directory, or the URL of a directory its protocol can list
* @param int_cb interrupt callback, asked between entries and by the protocol, may be NULL
* @param options options for avio_open_dir2(), which removes those it uses
* @return 0 or a negative error code
*/
int dir_list_read(DirList **dl, const char *path, const AVIOInterruptCB *int_cb, AVDictionary **options);
void dir_list_free(DirList **dl);
/**
* @return the absolute path or the URL of the entry, without a trailing slash, to be freed with av_free(), NULL on
* allocation failure
*/
char *dir_list_path(const DirList *dl, int entry);
#endif /* FFTOOLS_FFPLAY_DIR_H */
+14 -5
View File
@@ -598,7 +598,7 @@ static int list_configure(ListView *lv, const ListLayout *l, const SDL_Rect *can
return 0;
#else
if (!lv->libass_missing_reported++)
av_log(NULL, AV_LOG_ERROR, "The chapter list needs libass, which this build lacks\n");
av_log(NULL, AV_LOG_ERROR, "The list needs libass, which this build lacks\n");
return AVERROR(ENOSYS);
#endif
}
@@ -708,7 +708,7 @@ static int list_render(ListView *lv)
}
if (ret < 0) {
if (ret != AVERROR(ENOSYS))
av_log(NULL, AV_LOG_ERROR, "Could not render the chapter list: %s\n", av_err2str(ret));
av_log(NULL, AV_LOG_ERROR, "Could not render the list: %s\n", av_err2str(ret));
list_discard(lv);
av_freep(&strips);
nb = 0;
@@ -759,8 +759,7 @@ ListView *list_view_alloc(SDL_Renderer *renderer)
lv->playing = -1;
lv->mouse_x = lv->mouse_y = -1;
lv->hover = (ListHit){ 0, -1, -1, 0 };
for (int i = 0; i < LIST_SORT_NB; i++)
lv->sort[i].key = i;
list_view_clear(lv);
return lv;
}
@@ -825,6 +824,16 @@ int list_view_show(ListView *lv, const ListEntry *entries, int nb_entries, int s
return 0;
}
void list_view_clear(ListView *lv)
{
lv->nb_entries = lv->nb_rows = lv->selected = 0;
lv->search[0] = 0;
for (int i = 0; i < LIST_SORT_NB; i++)
lv->sort[i] = (ListSortKey){ i, 0 };
lv->shuffle_seed = 0;
lv->render_pending = 1;
}
void list_view_pin(ListView *lv, int pinned)
{
lv->pinned = pinned;
@@ -939,7 +948,7 @@ int list_view_update(ListView *lv)
{
int changed;
if (lv->render_pending)
if (lv->render_pending && lv->width)
list_render(lv);
changed = lv->changed || list_alpha(lv, av_gettime_relative()) != lv->drawn_alpha;
lv->changed = 0;
+7 -1
View File
@@ -37,12 +37,13 @@ typedef struct ListEntry {
const char *title;
const char *artist;
int64_t length; ///< in AV_TIME_BASE units, negative when unknown
const AVDictionary *metadata; ///< shown when the mouse is over the entry's symbol, may be NULL
AVDictionary *metadata; ///< shown when the mouse is over the entry's symbol, may be NULL
} ListEntry;
typedef struct ListView ListView;
/**
* @param renderer NULL when there is none, which leaves the list unable to show
* @return the list, hidden, or NULL on allocation failure
*/
ListView *list_view_alloc(SDL_Renderer *renderer);
@@ -59,6 +60,11 @@ void list_view_free(ListView **lv);
*/
int list_view_show(ListView *lv, const ListEntry *entries, int nb_entries, int selected);
/**
* Removes the entries, which may be freed after this, and resets the sort and the search.
*/
void list_view_clear(ListView *lv);
/**
* Keeps the list on screen and lets it take the keyboard and text input, or lets it fade out again.
*/