From d16d6eb53869e8380ccfbe7dbc2c09d98c40ab6c Mon Sep 17 00:00:00 2001 From: Lars-Dominik Braun Date: Mon, 21 Jul 2014 17:17:03 +0200 Subject: Refactor audio player, add retry timeout Fixes #442. --- src/config.h | 25 ++++++ src/main.h | 2 +- src/player.c | 276 +++++++++++++++++++++++++++++++++++++---------------------- src/player.h | 23 ++++- src/ui.c | 2 +- src/ui.h | 2 +- src/ui_act.c | 2 +- 7 files changed, 221 insertions(+), 111 deletions(-) diff --git a/src/config.h b/src/config.h index 6d7147d..fb8a041 100644 --- a/src/config.h +++ b/src/config.h @@ -6,4 +6,29 @@ #define VERSION "2014.06.08-dev" +/* ffmpeg/libav quirks detection + * ffmpeg’s micro versions always start at 100, that’s how we can distinguish + * ffmpeg and libav */ +#include + +/* is "timeout" option present (all versions of ffmpeg, not libav) */ +#if LIBAVFILTER_VERSION_MICRO >= 100 +#define HAVE_AV_TIMEOUT +#endif + +/* does graph_send_command exist (ffmpeg >=2.2) */ +#if LIBAVFILTER_VERSION_MAJOR == 4 && \ + LIBAVFILTER_VERSION_MICRO >= 100 +#define HAVE_AVFILTER_GRAPH_SEND_COMMAND +#endif + +/* need avcodec.h (ffmpeg 1.2) */ +#if LIBAVFILTER_VERSION_MAJOR == 3 && \ + LIBAVFILTER_VERSION_MINOR <= 42 && \ + LIBAVFILTER_VERSION_MINOR > 32 && \ + LIBAVFILTER_VERSION_MICRO >= 100 +#define HAVE_AV_BUFFERSINK_GET_BUFFER_REF +#define HAVE_LIBAVFILTER_AVCODEC_H +#endif + #endif /* SRC_CONFIG_H_S6A1C09K */ diff --git a/src/main.h b/src/main.h index 9567e06..12887ea 100644 --- a/src/main.h +++ b/src/main.h @@ -34,7 +34,7 @@ THE SOFTWARE. typedef struct { PianoHandle_t ph; WaitressHandle_t waith; - struct audioPlayer player; + player_t player; BarSettings_t settings; /* first item is current song */ PianoSong_t *playlist; diff --git a/src/player.c b/src/player.c index 3a2c9fa..5421310 100644 --- a/src/player.c +++ b/src/player.c @@ -31,28 +31,9 @@ THE SOFTWARE. #include #include -/* ffmpeg/libav quirks - * ffmpeg’s micro versions always start at 100, that’s how we can distinguish - * ffmpeg and libav */ -#include -/* ffmpeg >=2.2 */ -#if LIBAVFILTER_VERSION_MAJOR == 4 && \ - LIBAVFILTER_VERSION_MICRO >= 100 -#define HAVE_AVFILTER_GRAPH_SEND_COMMAND -#endif - -/* ffmpeg 1.2 */ -#if LIBAVFILTER_VERSION_MAJOR == 3 && \ - LIBAVFILTER_VERSION_MINOR <= 42 && \ - LIBAVFILTER_VERSION_MINOR > 32 && \ - LIBAVFILTER_VERSION_MICRO >= 100 -#define HAVE_AV_BUFFERSINK_GET_BUFFER_REF -#define HAVE_LIBAVFILTER_AVCODEC_H -#endif +#include "config.h" -#include #include -#include #include #include #include @@ -64,12 +45,17 @@ THE SOFTWARE. #endif #include #include +#ifndef HAVE_AV_TIMEOUT +#include +#endif #include "player.h" -#include "config.h" #include "ui.h" #include "ui_types.h" +/* default sample format */ +const enum AVSampleFormat avformat = AV_SAMPLE_FMT_S16; + static void printError (const BarSettings_t * const settings, const char * const msg, int ret) { char avmsg[128]; @@ -97,7 +83,7 @@ void BarPlayerDestroy () { /* Update volume filter */ -void BarPlayerSetVolume (struct audioPlayer * const player) { +void BarPlayerSetVolume (player_t * const player) { assert (player != NULL); assert (player->fvolume != NULL); @@ -124,51 +110,74 @@ void BarPlayerSetVolume (struct audioPlayer * const player) { #define softfail(msg) \ printError (player->settings, msg, ret); \ - pret = PLAYER_RET_SOFTFAIL; \ - goto finish; + return false; -/* player thread; for every song a new thread is started - * @param audioPlayer structure - * @return PLAYER_RET_* +#ifndef HAVE_AV_TIMEOUT +/* interrupt callback for libav, which lacks a timeout option + * + * obviously calling ping() a lot of times and then calling av_gettime here + * again is rather inefficient. */ -void *BarPlayerThread (void *data) { - assert (data != NULL); +static int intCb (void * const data) { + player_t * const player = data; + assert (player != NULL); + /* 10 seconds timeout (usec) */ + return (av_gettime () - player->ping) > 10*1000000; +} - struct audioPlayer * const player = data; - int ret; - intptr_t pret = PLAYER_RET_OK; - const enum AVSampleFormat avformat = AV_SAMPLE_FMT_S16; +#define ping() player->ping = av_gettime () +#else +#define ping() +#endif - ao_device *aoDev = NULL; - ao_sample_format aoFmt; +static bool openStream (player_t * const player) { + assert (player != NULL); + /* no leak? */ + assert (player->fctx == NULL); - AVFrame *frame = NULL, *filteredFrame = NULL; - AVFormatContext *fctx = NULL; - AVCodecContext *cctx = NULL; + int ret; /* stream setup */ - if ((ret = avformat_open_input (&fctx, player->url, NULL, NULL)) < 0) { + AVDictionary *options = NULL; +#ifdef HAVE_AV_TIMEOUT + /* 10 seconds timeout on TCP r/w */ + av_dict_set (&options, "timeout", "10000000", 0); +#else + /* libav does not support the timeout option above. the workaround stores + * the current time with ping() now and then, registers an interrupt + * callback (below) and compares saved/current time in this callback. it’s + * not bullet-proof, but seems to work fine for av_read_frame. */ + player->fctx = avformat_alloc_context (); + player->fctx->interrupt_callback.callback = intCb; + player->fctx->interrupt_callback.opaque = player; +#endif + + assert (player->url != NULL); + ping (); + if ((ret = avformat_open_input (&player->fctx, player->url, NULL, &options)) < 0) { softfail ("Unable to open audio file"); } - if ((ret = avformat_find_stream_info (fctx, NULL)) < 0) { + ping (); + if ((ret = avformat_find_stream_info (player->fctx, NULL)) < 0) { softfail ("find_stream_info"); } /* ignore all streams, undone for audio stream below */ - for (size_t i = 0; i < fctx->nb_streams; i++) { - fctx->streams[i]->discard = AVDISCARD_ALL; + for (size_t i = 0; i < player->fctx->nb_streams; i++) { + player->fctx->streams[i]->discard = AVDISCARD_ALL; } - const int streamIdx = av_find_best_stream (fctx, AVMEDIA_TYPE_AUDIO, -1, - -1, NULL, 0); - if (streamIdx < 0) { + ping (); + player->streamIdx = av_find_best_stream (player->fctx, AVMEDIA_TYPE_AUDIO, + -1, -1, NULL, 0); + if (player->streamIdx < 0) { softfail ("find_best_stream"); } - AVStream * const st = fctx->streams[streamIdx]; - cctx = st->codec; - st->discard = AVDISCARD_DEFAULT; + player->st = player->fctx->streams[player->streamIdx]; + AVCodecContext * const cctx = player->st->codec; + player->st->discard = AVDISCARD_DEFAULT; /* decoder setup */ AVCodec * const decoder = avcodec_find_decoder (cctx->codec_id); @@ -180,32 +189,39 @@ void *BarPlayerThread (void *data) { softfail ("codec_open2"); } - frame = avcodec_alloc_frame (); - assert (frame != NULL); - filteredFrame = avcodec_alloc_frame (); - assert (filteredFrame != NULL); + if (player->lastTimestamp > 0) { + ping (); + av_seek_frame (player->fctx, player->streamIdx, player->lastTimestamp, 0); + } - AVPacket pkt; - av_init_packet (&pkt); - pkt.data = NULL; - pkt.size = 0; + player->songPlayed = 0; + player->songDuration = av_q2d (player->st->time_base) * + (double) player->st->duration; + player->mode = PLAYER_PLAYING; + return true; +} + +/* setup filter chain + */ +static bool openFilter (player_t * const player) { /* filter setup */ char strbuf[256]; + int ret = 0; + AVCodecContext * const cctx = player->st->codec; if ((player->fgraph = avfilter_graph_alloc ()) == NULL) { softfail ("graph_alloc"); } /* abuffer */ - AVFilterContext *fabuf = NULL; - AVRational time_base = st->time_base; + AVRational time_base = player->st->time_base; snprintf (strbuf, sizeof (strbuf), "time_base=%d/%d:sample_rate=%d:sample_fmt=%s:channel_layout=0x%"PRIx64, time_base.num, time_base.den, cctx->sample_rate, av_get_sample_fmt_name (cctx->sample_fmt), cctx->channel_layout); - if ((ret = avfilter_graph_create_filter (&fabuf, + if ((ret = avfilter_graph_create_filter (&player->fabuf, avfilter_get_by_name ("abuffer"), NULL, strbuf, NULL, player->fgraph)) < 0) { softfail ("create_filter abuffer"); @@ -230,17 +246,16 @@ void *BarPlayerThread (void *data) { } /* abuffersink */ - AVFilterContext *fbufsink = NULL; - if ((ret = avfilter_graph_create_filter (&fbufsink, + if ((ret = avfilter_graph_create_filter (&player->fbufsink, avfilter_get_by_name ("abuffersink"), NULL, NULL, NULL, player->fgraph)) < 0) { softfail ("create_filter abuffersink"); } /* connect filter: abuffer -> volume -> aformat -> abuffersink */ - if (avfilter_link (fabuf, 0, player->fvolume, 0) != 0 || + if (avfilter_link (player->fabuf, 0, player->fvolume, 0) != 0 || avfilter_link (player->fvolume, 0, fafmt, 0) != 0 || - avfilter_link (fafmt, 0, fbufsink, 0) != 0) { + avfilter_link (fafmt, 0, player->fbufsink, 0) != 0) { softfail ("filter_link"); } @@ -248,7 +263,15 @@ void *BarPlayerThread (void *data) { softfail ("graph_config"); } - /* setup libao */ + return true; +} + +/* setup libao + */ +static bool openDevice (player_t * const player) { + AVCodecContext * const cctx = player->st->codec; + + ao_sample_format aoFmt; memset (&aoFmt, 0, sizeof (aoFmt)); aoFmt.bits = av_get_bytes_per_sample (avformat) * 8; assert (aoFmt.bits > 0); @@ -257,49 +280,64 @@ void *BarPlayerThread (void *data) { aoFmt.byte_format = AO_FMT_NATIVE; int driver = ao_default_driver_id (); - if ((aoDev = ao_open_live (driver, &aoFmt, NULL)) == NULL) { + if ((player->aoDev = ao_open_live (driver, &aoFmt, NULL)) == NULL) { BarUiMsg (player->settings, MSG_ERR, "Cannot open audio device.\n"); - pret = PLAYER_RET_HARDFAIL; - goto finish; + return false; } - player->songPlayed = 0; - player->songDuration = av_q2d (st->time_base) * (double) st->duration; - player->mode = PLAYER_PLAYING; + return true; +} + +/* decode and play stream. returns 0 or av error code. + */ +static int play (player_t * const player) { + assert (player != NULL); + + AVPacket pkt; + av_init_packet (&pkt); + pkt.data = NULL; + pkt.size = 0; + + AVFrame *frame = NULL, *filteredFrame = NULL; + frame = avcodec_alloc_frame (); + assert (frame != NULL); + filteredFrame = avcodec_alloc_frame (); + assert (filteredFrame != NULL); + + while (!player->doQuit) { + ping (); + int ret = av_read_frame (player->fctx, &pkt); + if (ret < 0) { + av_free_packet (&pkt); + return ret; + } else if (pkt.stream_index != player->streamIdx) { + av_free_packet (&pkt); + continue; + } - while (av_read_frame (fctx, &pkt) >= 0) { AVPacket pkt_orig = pkt; /* pausing */ pthread_mutex_lock (&player->pauseMutex); while (true) { if (!player->doPause) { - av_read_play (fctx); + av_read_play (player->fctx); break; } else { - av_read_pause (fctx); + av_read_pause (player->fctx); } pthread_cond_wait (&player->pauseCond, &player->pauseMutex); } pthread_mutex_unlock (&player->pauseMutex); - if (player->doQuit) { - av_free_packet (&pkt_orig); - break; - } - - if (pkt.stream_index != streamIdx) { - av_free_packet (&pkt_orig); - continue; - } - do { int got_frame = 0; - const int decoded = avcodec_decode_audio4 (cctx, frame, &got_frame, - &pkt); + const int decoded = avcodec_decode_audio4 (player->st->codec, + frame, &got_frame, &pkt); if (decoded < 0) { - softfail ("decode_audio4"); + /* skip this one */ + break; } if (got_frame != 0) { @@ -307,16 +345,17 @@ void *BarPlayerThread (void *data) { if (frame->pts == (int64_t) AV_NOPTS_VALUE) { frame->pts = 0; } - ret = av_buffersrc_write_frame (fabuf, frame); + ret = av_buffersrc_write_frame (player->fabuf, frame); assert (ret >= 0); while (true) { AVFilterBufferRef *audioref = NULL; #ifdef HAVE_AV_BUFFERSINK_GET_BUFFER_REF /* ffmpeg’s compatibility layer is broken in some releases */ - if (av_buffersink_get_buffer_ref (fbufsink, &audioref, 0) < 0) { + if (av_buffersink_get_buffer_ref (player->fbufsink, + &audioref, 0) < 0) { #else - if (av_buffersink_read (fbufsink, &audioref) < 0) { + if (av_buffersink_read (player->fbufsink, &audioref) < 0) { #endif /* try again next frame */ break; @@ -328,7 +367,7 @@ void *BarPlayerThread (void *data) { const int numChannels = av_get_channel_layout_nb_channels ( filteredFrame->channel_layout); const int bps = av_get_bytes_per_sample(filteredFrame->format); - ao_play (aoDev, (char *) filteredFrame->data[0], + ao_play (player->aoDev, (char *) filteredFrame->data[0], filteredFrame->nb_samples * numChannels * bps); avfilter_unref_bufferp (&audioref); @@ -341,26 +380,57 @@ void *BarPlayerThread (void *data) { av_free_packet (&pkt_orig); - player->songPlayed = av_q2d (st->time_base) * (double) pkt.pts; + player->songPlayed = av_q2d (player->st->time_base) * (double) pkt.pts; + player->lastTimestamp = pkt.pts; } -finish: - ao_close (aoDev); + avcodec_free_frame (&filteredFrame); + avcodec_free_frame (&frame); + + return 0; +} + +static void finish (player_t * const player) { + ao_close (player->aoDev); + player->aoDev = NULL; if (player->fgraph != NULL) { avfilter_graph_free (&player->fgraph); + player->fgraph = NULL; } - if (cctx != NULL) { - avcodec_close (cctx); + if (player->st != NULL && player->st->codec != NULL) { + avcodec_close (player->st->codec); + player->st = NULL; } - if (fctx != NULL) { - avformat_close_input (&fctx); - } - if (frame != NULL) { - avcodec_free_frame (&frame); - } - if (filteredFrame != NULL) { - avcodec_free_frame (&filteredFrame); + if (player->fctx != NULL) { + avformat_close_input (&player->fctx); } +} + +/* player thread; for every song a new thread is started + * @param audioPlayer structure + * @return PLAYER_RET_* + */ +void *BarPlayerThread (void *data) { + assert (data != NULL); + + player_t * const player = data; + intptr_t pret = PLAYER_RET_OK; + + bool retry = false; + do { + if (openStream (player)) { + if (openFilter (player) && openDevice (player)) { + retry = play (player) == AVERROR_INVALIDDATA; + } else { + /* filter missing or audio device busy */ + pret = PLAYER_RET_HARDFAIL; + } + } else { + /* stream not found */ + pret = PLAYER_RET_SOFTFAIL; + } + finish (player); + } while (retry); player->mode = PLAYER_FINISHED; diff --git a/src/player.h b/src/player.h index 2c89c93..eb06d64 100644 --- a/src/player.h +++ b/src/player.h @@ -31,14 +31,17 @@ THE SOFTWARE. #include #include +#include +#include #include #include #include #include #include "settings.h" +#include "config.h" -struct audioPlayer { +typedef struct { /* protected by pauseMutex */ volatile bool doQuit; volatile bool doPause; @@ -52,9 +55,21 @@ struct audioPlayer { PLAYER_FINISHED, } mode; + /* libav */ AVFilterContext *fvolume; AVFilterGraph *fgraph; - + AVFormatContext *fctx; + AVStream *st; + AVFilterContext *fbufsink, *fabuf; + int streamIdx; + int64_t lastTimestamp; +#ifndef HAVE_AV_TIMEOUT + int64_t ping; +#endif + + ao_device *aoDev; + + /* settings */ volatile double volume; double gain; char *url; @@ -63,12 +78,12 @@ struct audioPlayer { /* measured in seconds */ volatile unsigned int songDuration; volatile unsigned int songPlayed; -}; +} player_t; enum {PLAYER_RET_OK = 0, PLAYER_RET_HARDFAIL = 1, PLAYER_RET_SOFTFAIL = 2}; void *BarPlayerThread (void *data); -void BarPlayerSetVolume (struct audioPlayer * const player); +void BarPlayerSetVolume (player_t * const player); void BarPlayerInit (); void BarPlayerDestroy (); diff --git a/src/ui.c b/src/ui.c index 5d5b816..6259ba1 100644 --- a/src/ui.c +++ b/src/ui.c @@ -684,7 +684,7 @@ size_t BarUiListSongs (const BarSettings_t *settings, */ void BarUiStartEventCmd (const BarSettings_t *settings, const char *type, const PianoStation_t *curStation, const PianoSong_t *curSong, - const struct audioPlayer *player, PianoStation_t *stations, + const player_t * const player, PianoStation_t *stations, PianoReturn_t pRet, WaitressReturn_t wRet) { pid_t chld; int pipeFd[2]; diff --git a/src/ui.h b/src/ui.h index 96ad32f..60262f8 100644 --- a/src/ui.h +++ b/src/ui.h @@ -49,7 +49,7 @@ void BarUiPrintSong (const BarSettings_t *, const PianoSong_t *, const PianoStation_t *); size_t BarUiListSongs (const BarSettings_t *, const PianoSong_t *, const char *); void BarUiStartEventCmd (const BarSettings_t *, const char *, - const PianoStation_t *, const PianoSong_t *, const struct audioPlayer *, + const PianoStation_t *, const PianoSong_t *, const player_t *, PianoStation_t *, PianoReturn_t, WaitressReturn_t); int BarUiPianoCall (BarApp_t * const, PianoRequestType_t, void *, PianoReturn_t *, WaitressReturn_t *); diff --git a/src/ui_act.c b/src/ui_act.c index 91a4cf7..7515396 100644 --- a/src/ui_act.c +++ b/src/ui_act.c @@ -46,7 +46,7 @@ THE SOFTWARE. /* helper to _really_ skip a song (unlock mutex, quit player) * @param player handle */ -static inline void BarUiDoSkipSong (struct audioPlayer *player) { +static inline void BarUiDoSkipSong (player_t * const player) { assert (player != NULL); pthread_mutex_lock (&player->pauseMutex); -- cgit v1.2.3