diff --git a/audio.c b/audio.c new file mode 100644 index 0000000..69a324e --- /dev/null +++ b/audio.c @@ -0,0 +1,930 @@ +/// +/// @file audio.c @brief Audio module +/// +/// Copyright (c) 2009 - 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// +/// @defgroup Audio The audio module. +/// +/// This module contains all audio output functions. +/// +/// ALSA PCM api is used. +/// @see http://www.alsa-project.org/alsa-doc/alsa-lib +/// +/// alsa async playback is broken, don't use it! +/// + +#define USE_AUDIO_THREAD + +#include +#include + +#include +#define _(str) gettext(str) ///< gettext shortcut +#define _N(str) str ///< gettext_noop shortcut + +#include + +#ifdef USE_AUDIO_THREAD +#ifndef __USE_GNU +#define __USE_GNU +#endif +#include +#endif + +#include "ringbuffer.h" +#include "misc.h" +#include "audio.h" + +//---------------------------------------------------------------------------- +// Variables +//---------------------------------------------------------------------------- + +static const char *AudioPCMDevice; ///< alsa PCM device name +static const char *AudioMixerDevice; ///< alsa mixer device name +static volatile char AudioRunning; ///< thread running / stopped +static int AudioPaused; ///< audio paused +static unsigned AudioSampleRate; ///< audio sample rate in hz +static unsigned AudioChannels; ///< number of audio channels +static uint64_t AudioPTS; ///< audio pts clock + +//---------------------------------------------------------------------------- +// Alsa variables +//---------------------------------------------------------------------------- + +static snd_pcm_t *AlsaPCMHandle; ///< alsa pcm handle +static char AlsaCanPause; ///< hw supports pause +static int AlsaUseMmap; ///< use mmap + +static RingBuffer *AlsaRingBuffer; ///< audio ring buffer +static unsigned AlsaStartThreshold; ///< start play, if filled + +static snd_mixer_t *AlsaMixer; ///< alsa mixer handle +static snd_mixer_elem_t *AlsaMixerElem; ///< alsa pcm mixer element +static int AlsaRatio; ///< internal -> mixer ratio * 1000 + +//---------------------------------------------------------------------------- +// alsa pcm +//---------------------------------------------------------------------------- + +/** +** Place samples in ringbuffer. +** +** @param samples sample buffer +** @param count number of bytes in sample buffer +** +** @returns true if play should be started. +*/ +static int AlsaAddToRingbuffer(const void *samples, int count) +{ + int n; + + n = RingBufferWrite(AlsaRingBuffer, samples, count); + if (n != count) { + Error(_("audio/alsa: can't place %d samples in ring buffer\n"), count); + // too many bytes are lost + } + // Update audio clock + AudioPTS += (count * 90000) / (AudioSampleRate * AudioChannels * 2); + + if (!AudioRunning) { + if (AlsaStartThreshold < RingBufferUsedBytes(AlsaRingBuffer)) { + // restart play-back + return 1; + } + } + + return 0; +} + +/** +** Play samples from ringbuffer. +*/ +static int AlsaPlayRingbuffer(void) +{ + int first; + int avail; + int n; + int err; + int frames; + const void *p; + + first = 1; + for (;;) { + // how many bytes can be written? + n = snd_pcm_avail_update(AlsaPCMHandle); + if (n < 0) { + if (n == -EAGAIN) { + continue; + } + Error(_("audio/alsa: underrun error?\n")); + err = snd_pcm_recover(AlsaPCMHandle, n, 0); + if (err >= 0) { + continue; + } + Error(_("audio/alsa: snd_pcm_avail_update(): %s\n"), + snd_strerror(n)); + return -1; + } + avail = snd_pcm_frames_to_bytes(AlsaPCMHandle, n); + if (avail < 256) { // too much overhead + if (first) { + // happens with broken alsa drivers + Error(_("audio/alsa: broken driver %d\n"), avail); + } + break; + } + + n = RingBufferGetReadPointer(AlsaRingBuffer, &p); + if (!n) { // ring buffer empty + if (first) { // only error on first loop + return 1; + } + return 0; + } + if (n < avail) { // not enough bytes in ring buffer + avail = n; + } + if (!avail) { // full or buffer empty + break; + } + frames = snd_pcm_bytes_to_frames(AlsaPCMHandle, avail); + + again: + if (AlsaUseMmap) { + err = snd_pcm_mmap_writei(AlsaPCMHandle, p, frames); + } else { + err = snd_pcm_writei(AlsaPCMHandle, p, frames); + } + Debug(4, "audio/alsa: wrote %d/%d frames\n", err, frames); + if (err < 0) { + if (err == -EAGAIN) { + goto again; + } + Error(_("audio/alsa: underrun error?\n")); + err = snd_pcm_recover(AlsaPCMHandle, err, 0); + if (err >= 0) { + goto again; + } + Error(_("audio/alsa: snd_pcm_writei failed: %s\n"), + snd_strerror(err)); + return -1; + } + if (err != frames) { + // this could happen, if underrun happened + Error(_("audio/alsa: error not all frames written\n")); + avail = snd_pcm_frames_to_bytes(AlsaPCMHandle, err); + } + RingBufferReadAdvance(AlsaRingBuffer, avail); + first = 0; + } + + return 0; +} + +#if 0 + +// async playback is broken, don't use it! + +//---------------------------------------------------------------------------- +// async playback +//---------------------------------------------------------------------------- + +/** +** Alsa async pcm callback function. +** +** @param handler alsa async handler +*/ +static void AlsaAsyncCallback(snd_async_handler_t * handler) +{ + + Debug(3, "audio/%s: %p\n", __FUNCTION__, handler); + + // how many bytes can be written? + for (;;) { + n = snd_pcm_avail_update(AlsaPCMHandle); + if (n < 0) { + Error(_("audio/alsa: snd_pcm_avail_update(): %s\n"), + snd_strerror(n)); + break; + } + avail = snd_pcm_frames_to_bytes(AlsaPCMHandle, n); + if (avail < 512) { // too much overhead + break; + } + + n = RingBufferGetReadPointer(AlsaRingBuffer, &p); + if (!n) { // ring buffer empty + Debug(3, "audio/alsa: ring buffer empty\n"); + break; + } + if (n < avail) { // not enough bytes in ring buffer + avail = n; + } + if (!avail) { // full + break; + } + frames = snd_pcm_bytes_to_frames(AlsaPCMHandle, avail); + + again: + if (AlsaUseMmap) { + err = snd_pcm_mmap_writei(AlsaPCMHandle, p, frames); + } else { + err = snd_pcm_writei(AlsaPCMHandle, p, frames); + } + Debug(3, "audio/alsa: %d => %d\n", frames, err); + if (err < 0) { + Error(_("audio/alsa: underrun error?\n")); + err = snd_pcm_recover(AlsaPCMHandle, err, 0); + if (err >= 0) { + goto again; + } + Error(_("audio/alsa: snd_pcm_writei failed: %s\n"), + snd_strerror(err)); + } + if (err != frames) { + Error(_("audio/alsa: error not all frames written\n")); + avail = snd_pcm_frames_to_bytes(AlsaPCMHandle, err); + } + RingBufferReadAdvance(AlsaRingBuffer, avail); + } +} + +/** +** Place samples in audio output queue. +** +** @param samples sample buffer +** @param count number of bytes in sample buffer +*/ +void AudioEnqueue(const void *samples, int count) +{ + snd_pcm_state_t state; + int n; + + //int err; + + Debug(3, "audio: %6zd + %4d\n", RingBufferUsedBytes(AlsaRingBuffer), + count); + n = RingBufferWrite(AlsaRingBuffer, samples, count); + if (n != count) { + Fatal(_("audio: can't place %d samples in ring buffer\n"), count); + } + // check if running, wait until enough buffered + state = snd_pcm_state(AlsaPCMHandle); + if (state == SND_PCM_STATE_PREPARED) { + Debug(3, "audio/alsa: state %d - %s\n", state, + snd_pcm_state_name(state)); + // FIXME: adjust start ratio + if (RingBufferFreeBytes(AlsaRingBuffer) + < RingBufferUsedBytes(AlsaRingBuffer)) { + // restart play-back +#if 0 + if (AlsaCanPause) { + if ((err = snd_pcm_pause(AlsaPCMHandle, 0))) { + Error(_("audio: snd_pcm_pause(): %s\n"), + snd_strerror(err)); + } + } else { + if ((err = snd_pcm_prepare(AlsaPCMHandle)) < 0) { + Error(_("audio: snd_pcm_prepare(): %s\n"), + snd_strerror(err)); + } + } + if ((err = snd_pcm_prepare(AlsaPCMHandle)) < 0) { + Error(_("audio: snd_pcm_prepare(): %s\n"), snd_strerror(err)); + } + + Debug(3, "audio/alsa: unpaused\n"); + if ((err = snd_pcm_start(AlsaPCMHandle)) < 0) { + Error(_("audio: snd_pcm_start(): %s\n"), snd_strerror(err)); + } +#endif + state = snd_pcm_state(AlsaPCMHandle); + Debug(3, "audio/alsa: state %d - %s\n", state, + snd_pcm_state_name(state)); + Debug(3, "audio/alsa: unpaused\n"); + AudioPaused = 0; + } + } + // Update audio clock + // AudioPTS += (size * 90000) / (AudioSampleRate * AudioChannels * 2); +} + +#endif + +//---------------------------------------------------------------------------- +// thread playback +//---------------------------------------------------------------------------- + +#ifdef USE_AUDIO_THREAD + +static pthread_t AudioThread; ///< audio play thread +static pthread_cond_t AudioStartCond; ///< condition variable +static pthread_mutex_t AudioMutex; ///< audio condition mutex + +/** +** Audio play thread. +*/ +static void *AudioPlayHandlerThread(void *dummy) +{ + int err; + + Debug(3, "audio: play thread started\n"); + + for (;;) { + Debug(3, "audio: wait on start condition\n"); + pthread_mutex_lock(&AudioMutex); + AudioRunning = 0; + do { + pthread_cond_wait(&AudioStartCond, &AudioMutex); + // cond_wait can return, without signal! + } while (!AudioRunning); + pthread_mutex_unlock(&AudioMutex); + + Debug(3, "audio: play start\n"); + for (;;) { + Debug(4, "audio: play loop\n"); + pthread_testcancel(); + if ((err = snd_pcm_wait(AlsaPCMHandle, 100)) < 0) { + Error(_("audio/alsa: wait underrun error?\n")); + err = snd_pcm_recover(AlsaPCMHandle, err, 0); + if (err >= 0) { + continue; + } + Error(_("audio/alsa: snd_pcm_wait(): %s\n"), + snd_strerror(err)); + usleep(100 * 1000); + continue; + } + if ((err = AlsaPlayRingbuffer())) { // empty / error + snd_pcm_state_t state; + + if (err < 0) { // underrun error + break; + } + state = snd_pcm_state(AlsaPCMHandle); + if (state != SND_PCM_STATE_RUNNING) { + Debug(3, "audio/alsa: stopping play\n"); + break; + } + usleep(20 * 1000); + } + } + } + + return dummy; +} + +/** +** Place samples in audio output queue. +** +** @param samples sample buffer +** @param count number of bytes in sample buffer +*/ +void AudioEnqueue(const void *samples, int count) +{ + if (AlsaAddToRingbuffer(samples, count)) { + snd_pcm_state_t state; + + state = snd_pcm_state(AlsaPCMHandle); + Debug(3, "audio/alsa: enqueue state %s\n", snd_pcm_state_name(state)); + + // no lock needed, can wakeup next time + AudioRunning = 1; + pthread_cond_signal(&AudioStartCond); + } +} + +#endif + +//---------------------------------------------------------------------------- +// direct playback +//---------------------------------------------------------------------------- + +#if 0 + +// direct play produces underuns on some hardware + +/** +** Place samples in audio output queue. +** +** @param samples sample buffer +** @param count number of bytes in sample buffer +*/ +void AudioEnqueue(const void *samples, int count) +{ + snd_pcm_state_t state; + int avail; + int n; + int err; + int frames; + const void *p; + + Debug(3, "audio/alsa: %6zd + %4d\n", RingBufferUsedBytes(AlsaRingBuffer), + count); + n = RingBufferWrite(AlsaRingBuffer, samples, count); + if (n != count) { + Error(_("audio/alsa: can't place %d samples in ring buffer\n"), count); + } + // check if running, wait until enough buffered + state = snd_pcm_state(AlsaPCMHandle); + Debug(4, "audio/alsa: state %d - %s\n", state, snd_pcm_state_name(state)); + if (state == SND_PCM_STATE_PREPARED) { + // FIXME: adjust start ratio + if (RingBufferFreeBytes(AlsaRingBuffer) + > RingBufferUsedBytes(AlsaRingBuffer)) { + return; + } + Debug(3, "audio/alsa: state %d - %s start play\n", state, + snd_pcm_state_name(state)); + } + // Update audio clock + AudioPTS += (size * 90000) / (AudioSampleRate * AudioChannels * 2); +} + +#endif + +/** +** Initialize alsa pcm device. +** +** @see AudioPCMDevice +*/ +static void AlsaInitPCM(void) +{ + const char *device; + snd_pcm_t *handle; + snd_pcm_hw_params_t *hw_params; + int err; + snd_pcm_uframes_t buffer_size; + + if (!(device = AudioPCMDevice)) { + if (!(device = getenv("ALSA_DEVICE"))) { + device = "default"; + } + } + // FIXME: must set alsa error output to /dev/null + if ((err = + snd_pcm_open(&handle, device, SND_PCM_STREAM_PLAYBACK, + SND_PCM_NONBLOCK)) < 0) { + Fatal(_("audio/alsa: playback open '%s' error: %s\n"), device, + snd_strerror(err)); + } + AlsaPCMHandle = handle; + + if ((err = snd_pcm_nonblock(handle, SND_PCM_NONBLOCK)) < 0) { + Error(_("audio/alsa: can't set block mode: %s\n"), snd_strerror(err)); + } + + snd_pcm_hw_params_alloca(&hw_params); + // choose all parameters + if ((err = snd_pcm_hw_params_any(handle, hw_params)) < 0) { + Error(_ + ("audio: snd_pcm_hw_params_any: no configurations available: %s\n"), + snd_strerror(err)); + } + AlsaCanPause = snd_pcm_hw_params_can_pause(hw_params); + Info(_("audio/alsa: hw '%s' supports pause: %s\n"), device, + AlsaCanPause ? "yes" : "no"); + snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size); + Info(_("audio/alsa: max buffer size %lu\n"), buffer_size); + + pthread_mutex_init(&AudioMutex, NULL); + pthread_cond_init(&AudioStartCond, NULL); + pthread_create(&AudioThread, NULL, AudioPlayHandlerThread, NULL); + pthread_detach(AudioThread); +} + +//---------------------------------------------------------------------------- +// Alsa Mixer +//---------------------------------------------------------------------------- + +/** +** Set mixer volume (0-100) +** +** @param volume volume (0 .. 100) +*/ +void AudioSetVolume(int volume) +{ + int v; + + if (AlsaMixer && AlsaMixerElem) { + v = (volume * AlsaRatio) / 1000; + snd_mixer_selem_set_playback_volume(AlsaMixerElem, 0, v); + snd_mixer_selem_set_playback_volume(AlsaMixerElem, 1, v); + } +} + +/** +** Initialize alsa mixer. +*/ +static void AlsaInitMixer(void) +{ + const char *device; + snd_mixer_t *alsa_mixer; + snd_mixer_elem_t *alsa_mixer_elem; + long alsa_mixer_elem_min; + long alsa_mixer_elem_max; + + if (!(device = AudioMixerDevice)) { + if (!(device = getenv("ALSA_MIXER"))) { + device = "default"; + } + } + Debug(3, "audio/alsa: mixer open\n"); + snd_mixer_open(&alsa_mixer, 0); + if (alsa_mixer && snd_mixer_attach(alsa_mixer, device) >= 0 + && snd_mixer_selem_register(alsa_mixer, NULL, NULL) >= 0 + && snd_mixer_load(alsa_mixer) >= 0) { + + const char *const alsa_mixer_elem_name = "PCM"; + + alsa_mixer_elem = snd_mixer_first_elem(alsa_mixer); + while (alsa_mixer_elem) { + const char *name; + + name = snd_mixer_selem_get_name(alsa_mixer_elem); + if (strcasecmp(name, alsa_mixer_elem_name) == 0) { + snd_mixer_selem_get_playback_volume_range(alsa_mixer_elem, + &alsa_mixer_elem_min, &alsa_mixer_elem_max); + AlsaRatio = + (1000 * (alsa_mixer_elem_max - alsa_mixer_elem_min)) / 100; + Debug(3, "audio/alsa: PCM mixer found %ld - %ld ratio %d\n", + alsa_mixer_elem_min, alsa_mixer_elem_max, AlsaRatio); + break; + } + + alsa_mixer_elem = snd_mixer_elem_next(alsa_mixer_elem); + } + + AlsaMixer = alsa_mixer; + AlsaMixerElem = alsa_mixer_elem; + } else { + Error(_("audio/alsa: can't open alsa mixer '%s'\n"), device); + } +} + +//---------------------------------------------------------------------------- +//---------------------------------------------------------------------------- + +/** +** Get audio delay in time stamps. +*/ +uint64_t AudioGetDelay(void) +{ + int err; + snd_pcm_sframes_t delay; + + if ((err = snd_pcm_delay(AlsaPCMHandle, &delay)) < 0) { + //Debug(3, "audio/alsa: no hw delay\n"); + delay = 0UL; + } else if (snd_pcm_state(AlsaPCMHandle) != SND_PCM_STATE_RUNNING) { + //Debug(3, "audio/alsa: %lu delay ok, but not running\n", delay); + } + delay = (delay * 90000) / AudioSampleRate; + //Debug(3, "audio/alsa: hw delay %lu\n", delay); + delay += + (RingBufferUsedBytes(AlsaRingBuffer) * 90000) / (AudioSampleRate * + AudioChannels); + //Debug(3, "audio/alsa: hw+sw delay %lu ms\n", delay / 90); + + return delay; +} + +/** +** Setup audio for requested format. +** +** @param freq sample frequency +** @param channels number of channels +** +** @todo audio changes must be queued and done when the buffer is empty +*/ +void AudioSetup(int freq, int channels) +{ + snd_pcm_uframes_t buffer_size; + snd_pcm_uframes_t period_size; + int err; + +#if 1 + Debug(3, "audio/alsa: channels %d frequency %d hz\n", channels, freq); + + if (!freq || !channels) { // invalid parameter + // FIXME: set flag invalid setup + return; + } + + AudioChannels = channels; + AudioSampleRate = freq; + // FIXME: thread!! + RingBufferReadAdvance(AlsaRingBuffer, RingBufferUsedBytes(AlsaRingBuffer)); + + if ((err = + snd_pcm_set_params(AlsaPCMHandle, SND_PCM_FORMAT_S16, + AlsaUseMmap ? SND_PCM_ACCESS_MMAP_INTERLEAVED : + SND_PCM_ACCESS_RW_INTERLEAVED, channels, freq, 1, + 125 * 1000))) { + Error(_("audio/alsa: set params error: %s\n"), snd_strerror(err)); + if (channels == 2) { + // FIXME: must stop sound + return; + } + // FIXME: enable channel downmix + // AudioChannels = downmix_channels; + return; + } +#else + snd_pcm_hw_params_t *hw_params; + int dir; + unsigned buffer_time; + snd_pcm_uframes_t buffer_size; + + Debug(3, "audio/alsa: channels %d frequency %d hz\n", channels, freq); + + snd_pcm_hw_params_alloca(&hw_params); + // choose all parameters + if ((err = snd_pcm_hw_params_any(AlsaPCMHandle, hw_params)) < 0) { + Error(_ + ("audio: snd_pcm_hw_params_any: no configurations available: %s\n"), + snd_strerror(err)); + } + + if ((err = + snd_pcm_hw_params_set_rate_resample(AlsaPCMHandle, hw_params, 1)) + < 0) { + Error(_("audio: can't set rate resample: %s\n"), snd_strerror(err)); + } + if ((err = + snd_pcm_hw_params_set_format(AlsaPCMHandle, hw_params, + SND_PCM_FORMAT_S16)) < 0) { + Error(_("audio: can't set 16-bit: %s\n"), snd_strerror(err)); + } + if ((err = + snd_pcm_hw_params_set_access(AlsaPCMHandle, hw_params, + SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + Error(_("audio: can't set interleaved read/write %s\n"), + snd_strerror(err)); + } + if ((err = + snd_pcm_hw_params_set_channels(AlsaPCMHandle, hw_params, + channels)) < 0) { + Error(_("audio: can't set channels: %s\n"), snd_strerror(err)); + } + if ((err = + snd_pcm_hw_params_set_rate(AlsaPCMHandle, hw_params, freq, + 0)) < 0) { + Error(_("audio: can't set rate: %s\n"), snd_strerror(err)); + } + // 500000 + // 170667us + buffer_time = 1000 * 1000 * 1000; + dir = 1; +#if 0 + snd_pcm_hw_params_get_buffer_time_max(hw_params, &buffer_time, &dir); + Info(_("audio/alsa: %dus max buffer time\n"), buffer_time); + + buffer_time = 5 * 200 * 1000; // 1s + if ((err = + snd_pcm_hw_params_set_buffer_time_near(AlsaPCMHandle, hw_params, + &buffer_time, &dir)) < 0) { + Error(_("audio: snd_pcm_hw_params_set_buffer_time_near failed: %s\n"), + snd_strerror(err)); + } + Info(_("audio/alsa: %dus buffer time\n"), buffer_time); +#endif + snd_pcm_hw_params_get_buffer_size_max(hw_params, &buffer_size); + Info(_("audio/alsa: buffer size %lu\n"), buffer_size); + buffer_size = buffer_size < 65536 ? buffer_size : 65536; + if ((err = + snd_pcm_hw_params_set_buffer_size_near(AlsaPCMHandle, hw_params, + &buffer_size))) { + Error(_("audio: can't set buffer size: %s\n"), snd_strerror(err)); + } + Info(_("audio/alsa: buffer size %lu\n"), buffer_size); + + if ((err = snd_pcm_hw_params(AlsaPCMHandle, hw_params)) < 0) { + Error(_("audio: snd_pcm_hw_params failed: %s\n"), snd_strerror(err)); + } + // FIXME: use hw_params for buffer_size period_size +#endif + + // update buffer + + snd_pcm_get_params(AlsaPCMHandle, &buffer_size, &period_size); + Info(_("audio/alsa: buffer size %lu, period size %lu\n"), buffer_size, + period_size); + Debug(3, "audio/alsa: state %s\n", + snd_pcm_state_name(snd_pcm_state(AlsaPCMHandle))); + + AlsaStartThreshold = + snd_pcm_frames_to_bytes(AlsaPCMHandle, buffer_size + period_size); + // min 500ms + if (AlsaStartThreshold < (freq * channels * 2U) / 2) { + AlsaStartThreshold = (freq * channels * 2U) / 2; + } + Debug(3, "audio/alsa: delay %u ms\n", (AlsaStartThreshold * 1000) + / (AudioSampleRate * AudioChannels * 2)); +} + +/** +** Set alsa pcm audio device. +** +** @param device name of pcm device (fe. "hw:0,9") +*/ +void AudioSetDevice(const char *device) +{ + AudioPCMDevice = device; +} + +/** +** Initialize audio output module. +*/ +void AudioInit(void) +{ + AlsaRingBuffer = RingBufferNew(48000 * 8 * 2); // ~1s 8ch 16bit + + AlsaInitPCM(); + AlsaInitMixer(); + + AudioSetup(48000, 2); // set default parameters + + AudioPaused = 1; +} + +/** +** Cleanup audio output module. +*/ +void AudioExit(void) +{ + void *retval; + + pthread_cancel(AudioThread); + pthread_join(AudioThread, &retval); + if (retval != PTHREAD_CANCELED) { + Error(_("audio: can't cancel alsa play thread\n")); + } + pthread_cond_destroy(&AudioStartCond); + pthread_mutex_destroy(&AudioMutex); + + if (AlsaPCMHandle) { + snd_pcm_close(AlsaPCMHandle); + AlsaPCMHandle = NULL; + } + if (AlsaMixer) { + snd_mixer_close(AlsaMixer); + AlsaMixer = NULL; + AlsaMixerElem = NULL; + } + if (AlsaRingBuffer) { + RingBufferDel(AlsaRingBuffer); + AlsaRingBuffer = NULL; + } +} + +//---------------------------------------------------------------------------- +// Test +//---------------------------------------------------------------------------- + +void AudioTest(void) +{ + for (;;) { + unsigned u; + uint8_t buffer[16 * 1024]; // some random data + + for (u = 0; u < sizeof(buffer); u++) { + buffer[u] = random() & 0xffff; + } + + Debug(3, "audio/test: loop\n"); + for (;;) { + while (RingBufferFreeBytes(AlsaRingBuffer) > sizeof(buffer)) { + AudioEnqueue(buffer, sizeof(buffer)); + } + } + } +} + +#ifdef AUDIO_TEST + +#include + +int SysLogLevel; ///< show additional debug informations + +/** +** Print version. +*/ +static void PrintVersion(void) +{ + printf("audio_test: audio tester Version " VERSION +#ifdef GIT_REV + "(GIT-" GIT_REV ")" +#endif + ",\n\t(c) 2009 - 2011 by Johns\n" + "\tLicense AGPLv3: GNU Affero General Public License version 3\n"); +} + +/** +** Print usage. +*/ +static void PrintUsage(void) +{ + printf("Usage: audio_test [-?dhv]\n" + "\t-d\tenable debug, more -d increase the verbosity\n" + "\t-? -h\tdisplay this message\n" "\t-v\tdisplay version information\n" + "Only idiots print usage on stderr!\n"); +} + +/** +** Main entry point. +** +** @param argc number of arguments +** @param argv arguments vector +** +** @returns -1 on failures, 0 clean exit. +*/ +int main(int argc, char *const argv[]) +{ + SysLogLevel = 0; + + // + // Parse command line arguments + // + for (;;) { + switch (getopt(argc, argv, "hv?-c:d")) { + case 'd': // enabled debug + ++SysLogLevel; + continue; + + case EOF: + break; + case 'v': // print version + PrintVersion(); + return 0; + case '?': + case 'h': // help usage + PrintVersion(); + PrintUsage(); + return 0; + case '-': + PrintVersion(); + PrintUsage(); + fprintf(stderr, "\nWe need no long options\n"); + return -1; + case ':': + PrintVersion(); + fprintf(stderr, "Missing argument for option '%c'\n", optopt); + return -1; + default: + PrintVersion(); + fprintf(stderr, "Unkown option '%c'\n", optopt); + return -1; + } + break; + } + if (optind < argc) { + PrintVersion(); + while (optind < argc) { + fprintf(stderr, "Unhandled argument '%s'\n", argv[optind++]); + } + return -1; + } + // + // main loop + // + AudioInit(); + for (;;) { + unsigned u; + uint8_t buffer[16 * 1024]; // some random data + + for (u = 0; u < sizeof(buffer); u++) { + buffer[u] = random() & 0xffff; + } + + Debug(3, "audio/test: loop\n"); + for (;;) { + while (RingBufferFreeBytes(AlsaRingBuffer) > sizeof(buffer)) { + AudioEnqueue(buffer, sizeof(buffer)); + } + } + } + AudioExit(); + + return 0; +} + +#endif diff --git a/audio.h b/audio.h new file mode 100644 index 0000000..1f18134 --- /dev/null +++ b/audio.h @@ -0,0 +1,47 @@ +/// +/// @file audio.h @brief Audio module headerfile +/// +/// Copyright (c) 2009 - 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// @addtogroup Audio +/// @{ + +//---------------------------------------------------------------------------- +// Prototypes +//---------------------------------------------------------------------------- + +extern void AudioEnqueue(const void *, int); ///< buffer audio samples + +//extern int AudioFreeBytes(void); ///< free bytes in audio output +//extern int AudioUsedBytes(void); ///< used bytes in audio output +//extern void AudioSetClock(int64_t); ///< set audio clock base +//extern int64_t AudioGetClock(); ///< get current audio clock +extern uint64_t AudioGetDelay(void); ///< get current audio delay +extern void AudioSetup(int, int); ///< setup audio output + +//extern void AudioPlay(void); ///< play audio +//extern void AudioPause(void); ///< pause audio +extern void AudioSetVolume(int); ///< set volume + +extern void AudioSetDevice(const char *); ///< set alsa PCM audio device +extern void AudioInit(void); ///< setup audio module +extern void AudioExit(void); ///< cleanup and exit audio module + +/// @} diff --git a/codec.c b/codec.c new file mode 100644 index 0000000..8cc0124 --- /dev/null +++ b/codec.c @@ -0,0 +1,627 @@ +/// +/// @file codec.c @brief Codec functions +/// +/// Copyright (c) 2009 - 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// +/// @defgroup Codec The codec module. +/// +/// This module contains all decoder and codec functions. +/// It is uses ffmpeg (http://ffmpeg.org) as backend. +/// + +#include +#include + +#include +#include +#include +#include +#define _(str) gettext(str) ///< gettext shortcut +#define _N(str) str ///< gettext_noop shortcut + +#include +#include +#include + +#ifdef MAIN_H +#include MAIN_H +#endif +#include "misc.h" +#include "video.h" +#include "audio.h" +#include "codec.h" + +//---------------------------------------------------------------------------- +// Video +//---------------------------------------------------------------------------- + +#if 0 +/// +/// Video decoder typedef. +/// +//typedef struct _video_decoder_ Decoder; +#endif + +/// +/// Video decoder structure. +/// +struct _video_decoder_ +{ + VideoHwDecoder *HwDecoder; ///< video hardware decoder + + AVCodec *VideoCodec; ///< video codec + AVCodecContext *VideoCtx; ///< video codec context + AVFrame *Frame; ///< decoded video frame +}; + +//---------------------------------------------------------------------------- +// Call-backs +//---------------------------------------------------------------------------- + +static int CodecFfmpegOk; ///< check ffmpeg idiotics + +/** +** Callback to negotiate the PixelFormat. +** +** @param fmt is the list of formats which are supported by the codec, +** it is terminated by -1 as 0 is a valid format, the +** formats are ordered by quality. +*/ +static enum PixelFormat Codec_get_format(AVCodecContext * video_ctx, + const enum PixelFormat *fmt) +{ + VideoDecoder *decoder; + + decoder = video_ctx->opaque; + Debug(3, "codec: %s: %18p\n", __FUNCTION__, decoder); + CodecFfmpegOk = 1; + return Video_get_format(decoder->HwDecoder, video_ctx, fmt); +} + +/** +** Video buffer management, get buffer for frame. +** +** Called at the beginning of each frame to get a buffer for it. +** +** @param video_ctx Codec context +** @param frame Get buffer for this frame +*/ +static int Codec_get_buffer(AVCodecContext * video_ctx, AVFrame * frame) +{ + if (!CodecFfmpegOk) { // get_format missing + enum PixelFormat fmts[2]; + + fprintf(stderr, "codec: buggy ffmpeg\n"); + fmts[0] = video_ctx->pix_fmt; + fmts[1] = PIX_FMT_NONE; + Codec_get_format(video_ctx, fmts); + } + // VDPAU: PIX_FMT_VDPAU_H264 .. PIX_FMT_VDPAU_VC1 PIX_FMT_VDPAU_MPEG4 + if ((PIX_FMT_VDPAU_H264 <= video_ctx->pix_fmt + && video_ctx->pix_fmt <= PIX_FMT_VDPAU_VC1) + || video_ctx->pix_fmt == PIX_FMT_VDPAU_MPEG4) { + VideoDecoder *decoder; + unsigned surface; + + decoder = video_ctx->opaque; + surface = VideoGetSurface(decoder->HwDecoder); + + //Debug(3, "codec: use surface %#010x\n", surface); + + frame->type = FF_BUFFER_TYPE_USER; + frame->age = 256 * 256 * 256 * 64; + frame->data[0] = (void *)(size_t) surface; + + // FIXME: reordered? + return 0; + } + // VA-API: + if (video_ctx->hwaccel_context) { + VideoDecoder *decoder; + unsigned surface; + + decoder = video_ctx->opaque; + surface = VideoGetSurface(decoder->HwDecoder); + + //Debug(3, "codec: use surface %#010x\n", surface); + + frame->type = FF_BUFFER_TYPE_USER; + frame->age = 256 * 256 * 256 * 64; + // vaapi needs both fields set + frame->data[0] = (void *)(size_t) surface; + frame->data[3] = (void *)(size_t) surface; + + // FIXME: reordered? + return 0; + } + //Debug(3, "codec: fallback to default get_buffer\n"); + return avcodec_default_get_buffer(video_ctx, frame); +} + +/** +** Video buffer management, release buffer for frame. +** Called to release buffers which were allocated with get_buffer. +** +** @param video_ctx Codec context +** @param frame Release buffer for this frame +*/ +static void Codec_release_buffer(AVCodecContext * video_ctx, AVFrame * frame) +{ + // VDPAU: PIX_FMT_VDPAU_H264 .. PIX_FMT_VDPAU_VC1 PIX_FMT_VDPAU_MPEG4 + if ((PIX_FMT_VDPAU_H264 <= video_ctx->pix_fmt + && video_ctx->pix_fmt <= PIX_FMT_VDPAU_VC1) + || video_ctx->pix_fmt == PIX_FMT_VDPAU_MPEG4) { + VideoDecoder *decoder; + unsigned surface; + + decoder = video_ctx->opaque; + surface = (unsigned)(size_t) frame->data[0]; + + //Debug(3, "codec: release surface %#010x\n", surface); + VideoReleaseSurface(decoder->HwDecoder, surface); + + frame->data[0] = NULL; + + return; + } + // VA-API + if (video_ctx->hwaccel_context) { + VideoDecoder *decoder; + unsigned surface; + + decoder = video_ctx->opaque; + surface = (unsigned)(size_t) frame->data[3]; + + //Debug(3, "codec: release surface %#010x\n", surface); + VideoReleaseSurface(decoder->HwDecoder, surface); + + frame->data[0] = NULL; + frame->data[3] = NULL; + + return; + } + //Debug(3, "codec: fallback to default release_buffer\n"); + return avcodec_default_release_buffer(video_ctx, frame); +} + +//---------------------------------------------------------------------------- +// Test +//---------------------------------------------------------------------------- + +/** +** Allocate a new video decoder context. +** +** @param hw_decoder video hardware decoder +** +** @returns private decoder pointer for audio/video decoder. +*/ +VideoDecoder *CodecVideoNewDecoder(VideoHwDecoder * hw_decoder) +{ + VideoDecoder *decoder; + + if (!(decoder = calloc(1, sizeof(*decoder)))) { + Fatal(_("codec: Can't allocate vodeo decoder\n")); + } + decoder->HwDecoder = hw_decoder; + + return decoder; +} + +/** +** Open video decoder. +** +** @param decoder private video decoder +** @param name video codec name +** @param codec_id video codec id, used if name == NULL +*/ +void CodecVideoOpen(VideoDecoder * decoder, const char *name, int codec_id) +{ + AVCodec *video_codec; + + Debug(3, "codec: using codec %s or ID %#04x\n", name, codec_id); + + // + // ffmpeg compatibility hack + // +#if LIBAVCODEC_VERSION_INT <= AV_VERSION_INT(52,96,0) + if (name) { + if (!strcmp(name, "h264video_vdpau")) { + name = "h264_vdpau"; + } else if (!strcmp(name, "mpeg4video_vdpau")) { + name = "mpeg4_vdpau"; + } else if (!strcmp(name, "vc1video_vdpau")) { + name = "vc1_vdpau"; + } else if (!strcmp(name, "wmv3video_vdpau")) { + name = "wmv3_vdpau"; + } + } +#endif + + if (name && (video_codec = avcodec_find_decoder_by_name(name))) { + Debug(3, "codec: vdpau decoder found\n"); + } else if (!(video_codec = avcodec_find_decoder(codec_id))) { + Fatal(_("codec: codec ID %#04x not found\n"), codec_id); + } + decoder->VideoCodec = video_codec; + + if (!(decoder->VideoCtx = avcodec_alloc_context3(video_codec))) { + Fatal(_("codec: can't allocate video codec context\n")); + } + // open codec + if (avcodec_open2(decoder->VideoCtx, video_codec, NULL) < 0) { + Fatal(_("codec: can't open video codec!\n")); + } + + decoder->VideoCtx->opaque = decoder; // our structure + + /* + // FIXME: the number of cpu's should be configurable + // Today this makes no big sense H264 is broken with current streams. + avcodec_thread_init(decoder->VideoCtx, 2); // support dual-cpu's + */ + + Debug(3, "codec: video '%s'\n", decoder->VideoCtx->codec_name); + if (codec_id == CODEC_ID_H264) { + // 2.53 Ghz CPU is too slow for this codec at 1080i + //decoder->VideoCtx->skip_loop_filter = AVDISCARD_ALL; + //decoder->VideoCtx->skip_loop_filter = AVDISCARD_BIDIR; + } + if (video_codec->capabilities & CODEC_CAP_TRUNCATED) { + Debug(3, "codec: video can use truncated packets\n"); + // we do not send complete frames + decoder->VideoCtx->flags |= CODEC_FLAG_TRUNCATED; + } + // FIXME: own memory management for video frames. + if (video_codec->capabilities & CODEC_CAP_DR1) { + Debug(3, "codec: can use own buffer management\n"); + } + if (video_codec->capabilities & CODEC_CAP_HWACCEL_VDPAU) { + Debug(3, "codec: can export data for HW decoding (VDPAU)\n"); + } +#ifdef CODEC_CAP_FRAME_THREADS + if (video_codec->capabilities & CODEC_CAP_FRAME_THREADS) { + Debug(3, "codec: codec supports frame threads\n"); + } +#endif + //decoder->VideoCtx->debug = FF_DEBUG_STARTCODE; + + if (video_codec->capabilities & CODEC_CAP_HWACCEL_VDPAU) { + // FIXME: get_format never called. + decoder->VideoCtx->get_format = Codec_get_format; + decoder->VideoCtx->get_buffer = Codec_get_buffer; + decoder->VideoCtx->release_buffer = Codec_release_buffer; + decoder->VideoCtx->reget_buffer = Codec_get_buffer; + //decoder->VideoCtx->draw_horiz_band = Codec_draw_horiz_band; + } else { + decoder->VideoCtx->hwaccel_context = + VideoGetVaapiContext(decoder->HwDecoder); + } + + // our pixel format video hardware decoder hook + if (decoder->VideoCtx->hwaccel_context) { + decoder->VideoCtx->get_format = Codec_get_format; + decoder->VideoCtx->get_buffer = Codec_get_buffer; + decoder->VideoCtx->release_buffer = Codec_release_buffer; + decoder->VideoCtx->reget_buffer = Codec_get_buffer; +#if 0 + decoder->VideoCtx->thread_count = 1; + decoder->VideoCtx->draw_horiz_band = NULL; + decoder->VideoCtx->slice_flags = + SLICE_FLAG_CODED_ORDER | SLICE_FLAG_ALLOW_FIELD; + //decoder->VideoCtx->flags |= CODEC_FLAG_EMU_EDGE; +#endif + } + // + // Prepare frame buffer for decoder + // + if (!(decoder->Frame = avcodec_alloc_frame())) { + Fatal(_("codec: can't allocate decoder frame\n")); + } +} + +/** +** Close video decoder. +** +** @param video_decoder private video decoder +*/ +void CodecVideoClose(VideoDecoder * video_decoder) +{ + (void)video_decoder; + // FIXME: write close code +} + +#if 0 + +#ifdef DEBUG + +/** +** Display pts... +*/ +void DisplayPts(AVCodecContext * video_ctx, AVFrame * frame) +{ + int ms_delay; + + if (frame->pts == (int64_t) AV_NOPTS_VALUE) { + printf("*"); + } + ms_delay = (1000 * video_ctx->time_base.num) / video_ctx->time_base.den; + ms_delay += frame->repeat_pict * ms_delay / 2; + printf("codec: PTS %s%s %" PRId64 " %d/%d %dms\n", + frame->repeat_pict ? "r" : " ", frame->interlaced_frame ? "I" : " ", + frame->pts, video_ctx->time_base.num, video_ctx->time_base.den, + ms_delay); +} +#endif + +#endif + +/** +** Decode a video packet. +** +** @param decoder video decoder data +** @param avpkt video packet +** +** @note this version destroys avpkt!! +*/ +void CodecVideoDecode(VideoDecoder * decoder, AVPacket * avpkt) +{ + AVCodecContext *video_ctx; + AVFrame *frame; + int used; + int got_frame; + + video_ctx = decoder->VideoCtx; + frame = decoder->Frame; + + next_part: + // FIXME: this function can crash with bad packets + used = avcodec_decode_video2(video_ctx, frame, &got_frame, avpkt); + Debug(4, "%s: %p %d -> %d %d\n", __FUNCTION__, avpkt->data, avpkt->size, + used, got_frame); + + if (got_frame) { // frame completed + //DisplayPts(video_ctx, frame); + if (video_ctx->hwaccel_context) { + VideoRenderFrame(decoder->HwDecoder, video_ctx, frame); + } else { + VideoRenderFrame(decoder->HwDecoder, video_ctx, frame); + } + } else { + // some frames are needed for references, interlaced frames ... + // could happen with h264 dvb streams, just drop data. + + Debug(4, "codec: %8d incomplete interlaced frame %d bytes used\n", + video_ctx->frame_number, used); + } + if (used != avpkt->size) { + if (used == 0) { + goto next_part; + } + if (used >= 0) { + // some tv channels, produce this + Debug(4, + "codec: ooops didn't use complete video packet used %d of %d\n", + used, avpkt->size); + avpkt->data += used; + avpkt->size -= used; + goto next_part; + } + Debug(3, "codec: bad frame %d\n", used); + } + + return; +} + +//---------------------------------------------------------------------------- +// Audio +//---------------------------------------------------------------------------- + +#if 0 +/// +/// Audio decoder typedef. +/// +typedef struct _audio_decoder_ AudioDecoder; +#endif + +/// +/// Audio decoder structure. +/// +struct _audio_decoder_ +{ + AVCodec *AudioCodec; ///< audio codec + AVCodecContext *AudioCtx; ///< audio codec context + + /// audio parser to support wired dvb streaks + AVCodecParserContext *AudioParser; + int SampleRate; ///< old sample rate + int Channels; ///< old channels +}; + +/** +** Allocate a new audio decoder context. +** +** @param hw_decoder video hardware decoder +** +** @returns private decoder pointer for audio/video decoder. +*/ +AudioDecoder *CodecAudioNewDecoder(void) +{ + AudioDecoder *audio_decoder; + + if (!(audio_decoder = calloc(1, sizeof(*audio_decoder)))) { + Fatal(_("codec: Can't allocate audio decoder\n")); + } + + return audio_decoder; +} + +/** +** Open audio decoder. +** +** @param audio_decoder private audio decoder +** @param name audio codec name +** @param codec_id audio codec id, used if name == NULL +*/ +void CodecAudioOpen(AudioDecoder * audio_decoder, const char *name, + int codec_id) +{ + AVCodec *audio_codec; + + if (name && (audio_codec = avcodec_find_decoder_by_name(name))) { + Debug(3, "codec: audio decoder '%s' found\n", name); + } else if (!(audio_codec = avcodec_find_decoder(codec_id))) { + Fatal(_("codec: codec ID %#04x not found\n"), codec_id); + // FIXME: errors aren't fatal + } + audio_decoder->AudioCodec = audio_codec; + + if (!(audio_decoder->AudioCtx = avcodec_alloc_context3(audio_codec))) { + Fatal(_("codec: can't allocate audio codec context\n")); + } + // open codec + if (avcodec_open2(audio_decoder->AudioCtx, audio_codec, NULL) < 0) { + Fatal(_("codec: can't open audio codec\n")); + } + Debug(3, "codec: audio '%s'\n", audio_decoder->AudioCtx->codec_name); + + if (audio_codec->capabilities & CODEC_CAP_TRUNCATED) { + Debug(3, "codec: audio can use truncated packets\n"); + // we do not send complete frames + audio_decoder->AudioCtx->flags |= CODEC_FLAG_TRUNCATED; + } + if (!(audio_decoder->AudioParser = + av_parser_init(audio_decoder->AudioCtx->codec_id))) { + Fatal(_("codec: can't init audio parser\n")); + } +} + +/** +** Close audio decoder. +** +** @param audio_decoder private audio decoder +*/ +void CodecAudioClose(AudioDecoder * audio_decoder) +{ + // FIXME: output any buffered data + if (audio_decoder->AudioParser) { + av_parser_close(audio_decoder->AudioParser); + audio_decoder->AudioParser = NULL; + } + if (audio_decoder->AudioCtx) { + avcodec_close(audio_decoder->AudioCtx); + av_freep(&audio_decoder->AudioCtx); + } +} + +/** +** Decode an audio packet. +** +** PTS must be handled self. +** +** @param audio_decoder audio_Decoder data +** @param avpkt audio packet +*/ +void CodecAudioDecode(AudioDecoder * audio_decoder, AVPacket * avpkt) +{ + int16_t buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 4 + + FF_INPUT_BUFFER_PADDING_SIZE] __attribute__ ((aligned(16))); + AVCodecContext *audio_ctx; + int index; + AVPacket spkt[1]; + + if (!audio_decoder->AudioParser) { + Fatal(_("codec: internal error parser freeded while running\n")); + } + + if (av_new_packet(spkt, avpkt->size + FF_INPUT_BUFFER_PADDING_SIZE)) { + Error(_("codec: out of memory\n")); + return; + } + memcpy(spkt->data, avpkt->data, avpkt->size); + memset(spkt->data + avpkt->size, 0, FF_INPUT_BUFFER_PADDING_SIZE); + audio_ctx = audio_decoder->AudioCtx; + index = 0; + while (avpkt->size > index) { + int n; + int l; + AVPacket dpkt[1]; + + av_init_packet(dpkt); + n = av_parser_parse2(audio_decoder->AudioParser, audio_ctx, + &dpkt->data, &dpkt->size, spkt->data + index, avpkt->size - index, + AV_NOPTS_VALUE, AV_NOPTS_VALUE, AV_NOPTS_VALUE); + + if (dpkt->size) { + int buf_sz; + + buf_sz = sizeof(buf); + l = avcodec_decode_audio3(audio_ctx, buf, &buf_sz, dpkt); + if (l < 0) { // no audio frame could be decompressed + Error(_("codec: error audio data\n")); + break; + } +#ifdef notyetFF_API_OLD_DECODE_AUDIO + // FIXME: ffmpeg git comeing + int got_frame; + + avcodec_decode_audio4(audio_ctx, frame, &got_frame, dpkt); +#else +#endif + // FIXME: must first play remainings bytes, than change and play new. + if (audio_decoder->SampleRate != audio_ctx->sample_rate + || audio_decoder->Channels != audio_ctx->channels) { + + // FIXME: channels not support? + AudioSetup(audio_ctx->sample_rate, audio_ctx->channels); + + audio_decoder->SampleRate = audio_ctx->sample_rate; + audio_decoder->Channels = audio_ctx->channels; + } + AudioEnqueue(buf, buf_sz); + + if (dpkt->size > l) { + Error(_("codec: error more than one frame data\n")); + } + } + + index += n; + } + av_destruct_packet(spkt); +} + +//---------------------------------------------------------------------------- +// Codec +//---------------------------------------------------------------------------- + +/** +** Codec init +*/ +void CodecInit(void) +{ + avcodec_register_all(); // register all formats and codecs +} + +/** +** Codec exit. +*/ +void CodecExit(void) +{ +} diff --git a/codec.h b/codec.h new file mode 100644 index 0000000..855738d --- /dev/null +++ b/codec.h @@ -0,0 +1,70 @@ +/// +/// @file codec.h @brief Codec module headerfile +/// +/// Copyright (c) 2009 - 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// @addtogroup Codec +/// @{ + +//---------------------------------------------------------------------------- +// Typedefs +//---------------------------------------------------------------------------- + + /// Video decoder typedef. +typedef struct _video_decoder_ VideoDecoder; + + /// Audio decoder typedef. +typedef struct _audio_decoder_ AudioDecoder; + +//---------------------------------------------------------------------------- +// Prototypes +//---------------------------------------------------------------------------- + + /// Allocate a new video decoder context. +extern VideoDecoder *CodecVideoNewDecoder(VideoHwDecoder *); + + /// Open video codec +extern void CodecVideoOpen(VideoDecoder *, const char *, int); + + /// Close video codec +extern void CodecVideoClose(VideoDecoder *); + + /// Decode a video packet +extern void CodecVideoDecode(VideoDecoder *, AVPacket * pkt); + + /// Allocate a new audio decoder context. +extern AudioDecoder *CodecAudioNewDecoder(void); + + /// Open audio codec +extern void CodecAudioOpen(AudioDecoder *, const char *, int); + + /// Close audio codec +extern void CodecAudioClose(AudioDecoder *); + + /// Decode an audio packet +extern void CodecAudioDecode(AudioDecoder *, AVPacket * pkt); + + /// Setup and initialize codec module. +extern void CodecInit(void); + + /// Cleanup and exit codec module. +extern void CodecExit(void); + +/// @} diff --git a/misc.h b/misc.h new file mode 100644 index 0000000..69367df --- /dev/null +++ b/misc.h @@ -0,0 +1,119 @@ +/// +/// @file misc.h @brief Misc function header file +/// +/// Copyright (c) 2009 - 2011 by Lutz Sammer. All Rights Reserved. +/// +/// Contributor(s): +/// Copied from uwm. +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// @addtogroup misc +/// @{ + +#include +#include +#include // clock_gettime + +////////////////////////////////////////////////////////////////////////////// +// Defines +////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////////// +// Declares +////////////////////////////////////////////////////////////////////////////// + +////////////////////////////////////////////////////////////////////////////// +// Variables +////////////////////////////////////////////////////////////////////////////// + +extern int SysLogLevel; ///< how much information wanted + +////////////////////////////////////////////////////////////////////////////// +// Prototypes +////////////////////////////////////////////////////////////////////////////// + +static inline void Debug(const int, const char *format, ...) + __attribute__ ((format(printf, 2, 3))); + +////////////////////////////////////////////////////////////////////////////// +// Inlines +////////////////////////////////////////////////////////////////////////////// + +#define DebugLevel 4 /// private debug level + +/** +** Debug output function. +** +** - 0 fatal errors and errors +** - 1 warnings +** - 2 info +** - 3 important debug and fixme's +*/ +static inline void Debug(const int level, const char *format, ...) +{ + if (SysLogLevel > level || DebugLevel > level) { + va_list ap; + + va_start(ap, format); + vsyslog(LOG_ERR, format, ap); + va_end(ap); + } +} + +/** +** Show error. +*/ +#define Error(fmt...) Debug(0, fmt) + +/** +** Show fatal error. +*/ +#define Fatal(fmt...) do { Error(fmt); exit(-1); } while (0) + +/** +** Show warning. +*/ +#define Warning(fmt...) Debug(1, fmt) + +/** +** Show info. +*/ +#define Info(fmt...) Debug(2, fmt) + +/** +** Get ticks in ms. +** +** @returns ticks in ms, +*/ +static inline uint32_t GetMsTicks(void) +{ +#ifdef CLOCK_MONOTONIC + struct timespec tspec; + + clock_gettime(CLOCK_MONOTONIC, &tspec); + return (tspec.tv_sec * 1000) + (tspec.tv_nsec / (1000 * 1000)); +#else + struct timeval tval; + + if (gettimeofday(&tval, NULL) < 0) { + return 0; + } + return (tval.tv_sec * 1000) + (tval.tv_usec / 1000); +#endif +} + +/// @} diff --git a/ringbuffer.c b/ringbuffer.c new file mode 100644 index 0000000..81722e2 --- /dev/null +++ b/ringbuffer.c @@ -0,0 +1,328 @@ +/// +/// @file ringbuffer.c @brief Ringbuffer module +/// +/// Copyright (c) 2009, 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// +/// @defgroup Ringbuffer The ring buffer module. +/// +/// Lock free ring buffer with only one writer and one reader. +/// + +#include +#include +#include + +#include + +#include "ringbuffer.h" + + /// ring buffer structure +struct _ring_buffer_ +{ + char *Buffer; ///< ring buffer data + const char *BufferEnd; ///< end of buffer + size_t Size; ///< bytes in buffer (for faster calc) + + const char *ReadPointer; ///< only used by reader + char *WritePointer; ///< only used by writer + + /// The only thing modified by both + atomic_t Filled; ///< how many of the buffer is used +}; + +/** +** Allocate a new ring buffer. +** +** @param size Size of the ring buffer. +** +** @returns Allocated ring buffer, must be freed with +** RingBufferDel(), NULL for out of memory. +*/ +RingBuffer *RingBufferNew(size_t size) +{ + RingBuffer *rb; + + if (!(rb = malloc(sizeof(*rb)))) { // allocate structure + return rb; + } + if (!(rb->Buffer = malloc(size))) { // allocate buffer + free(rb); + return NULL; + } + + rb->Size = size; + rb->ReadPointer = rb->Buffer; + rb->WritePointer = rb->Buffer; + rb->BufferEnd = rb->Buffer + size; + atomic_set(&rb->Filled, 0); + + return rb; +} + +/** +** Free an allocated ring buffer. +*/ +void RingBufferDel(RingBuffer * rb) +{ + free(rb->Buffer); + free(rb); +} + +/** +** Advance write pointer in ring buffer. +** +** @param rb Ring buffer to adance write pointer. +** @param cnt Number of bytes to be adavanced. +** +** @returns Number of bytes that could be advanced in ring buffer. +*/ +size_t RingBufferWriteAdvance(RingBuffer * rb, size_t cnt) +{ + size_t n; + + n = rb->Size - atomic_read(&rb->Filled); + if (cnt > n) { // not enough space + cnt = n; + } + // + // Hitting end of buffer? + // + n = rb->BufferEnd - rb->WritePointer; + if (n > cnt) { // don't cross the end + rb->WritePointer += cnt; + } else { // reached or cross the end + rb->WritePointer = rb->Buffer; + if (n < cnt) { + n = cnt - n; + rb->WritePointer += n; + } + } + + // + // Only atomic modification! + // + atomic_add(cnt, &rb->Filled); + return cnt; +} + +/** +** Write to a ring buffer. +** +** @param rb Ring buffer to write to. +** @param buf Buffer of @p cnt bytes. +** @param cnt Number of bytes in buffer. +** +** @returns The number of bytes that could be placed in the ring +** buffer. +*/ +size_t RingBufferWrite(RingBuffer * rb, const void *buf, size_t cnt) +{ + size_t n; + + n = rb->Size - atomic_read(&rb->Filled); + if (cnt > n) { // not enough space + cnt = n; + } + // + // Hitting end of buffer? + // + n = rb->BufferEnd - rb->WritePointer; + if (n > cnt) { // don't cross the end + memcpy(rb->WritePointer, buf, cnt); + rb->WritePointer += cnt; + } else { // reached or cross the end + memcpy(rb->WritePointer, buf, n); + rb->WritePointer = rb->Buffer; + if (n < cnt) { + buf += n; + n = cnt - n; + memcpy(rb->WritePointer, buf, n); + rb->WritePointer += n; + } + } + + // + // Only atomic modification! + // + atomic_add(cnt, &rb->Filled); + return cnt; +} + +/** +** Get write pointer and free bytes at this position of ring buffer. +** +** @param rb Ring buffer to write to. +** @param[out] wp Write pointer is placed here +** +** @returns The number of bytes that could be placed in the ring +** buffer at the write pointer. +*/ +size_t RingBufferGetWritePointer(RingBuffer * rb, void **wp) +{ + size_t n; + size_t cnt; + + // Total free bytes available in ring buffer + cnt = rb->Size - atomic_read(&rb->Filled); + + *wp = rb->WritePointer; + + // + // Hitting end of buffer? + // + n = rb->BufferEnd - rb->WritePointer; + if (n <= cnt) { // reached or cross the end + return n; + } + return cnt; +} + +/** +** Advance read pointer in ring buffer. +** +** @param rb Ring buffer to adance read pointer. +** @param cnt Number of bytes to be advanced. +** +** @returns Number of bytes that could be advanced in ring buffer. +*/ +size_t RingBufferReadAdvance(RingBuffer * rb, size_t cnt) +{ + size_t n; + + n = atomic_read(&rb->Filled); + if (cnt > n) { // not enough filled + cnt = n; + } + // + // Hitting end of buffer? + // + n = rb->BufferEnd - rb->ReadPointer; + if (n > cnt) { // don't cross the end + rb->ReadPointer += cnt; + } else { // reached or cross the end + rb->ReadPointer = rb->Buffer; + if (n < cnt) { + n = cnt - n; + rb->ReadPointer += n; + } + } + + // + // Only atomic modification! + // + atomic_sub(cnt, &rb->Filled); + return cnt; +} + +/** +** Read from a ring buffer. +** +** @param rb Ring buffer to read from. +** @param buf Buffer of @p cnt bytes. +** @param cnt Number of bytes to be read. +** +** @returns Number of bytes that could be read from ring buffer. +*/ +size_t RingBufferRead(RingBuffer * rb, void *buf, size_t cnt) +{ + size_t n; + + n = atomic_read(&rb->Filled); + if (cnt > n) { // not enough filled + cnt = n; + } + // + // Hitting end of buffer? + // + n = rb->BufferEnd - rb->ReadPointer; + if (n > cnt) { // don't cross the end + memcpy(buf, rb->ReadPointer, cnt); + rb->ReadPointer += cnt; + } else { // reached or cross the end + memcpy(buf, rb->ReadPointer, n); + rb->ReadPointer = rb->Buffer; + if (n < cnt) { + buf += n; + n = cnt - n; + memcpy(buf, rb->ReadPointer, n); + rb->ReadPointer += n; + } + } + + // + // Only atomic modification! + // + atomic_sub(cnt, &rb->Filled); + return cnt; +} + +/** +** Get read pointer and used bytes at this position of ring buffer. +** +** @param rb Ring buffer to read from. +** @param[out] rp Read pointer is placed here +** +** @returns The number of bytes that could be read from the ring +** buffer at the read pointer. +*/ +size_t RingBufferGetReadPointer(RingBuffer * rb, const void **rp) +{ + size_t n; + size_t cnt; + + // Total used bytes in ring buffer + cnt = atomic_read(&rb->Filled); + + *rp = rb->ReadPointer; + + // + // Hitting end of buffer? + // + n = rb->BufferEnd - rb->ReadPointer; + if (n <= cnt) { // reached or cross the end + return n; + } + return cnt; +} + +/** +** Get free bytes in ring buffer. +** +** @param rb Ring buffer. +** +** @returns Number of bytes free in buffer. +*/ +size_t RingBufferFreeBytes(RingBuffer * rb) +{ + return rb->Size - atomic_read(&rb->Filled); +} + +/** +** Get used bytes in ring buffer. +** +** @param rb Ring buffer. +** +** @returns Number of bytes used in buffer. +*/ +size_t RingBufferUsedBytes(RingBuffer * rb) +{ + return atomic_read(&rb->Filled); +} diff --git a/ringbuffer.h b/ringbuffer.h new file mode 100644 index 0000000..a5e8a68 --- /dev/null +++ b/ringbuffer.h @@ -0,0 +1,59 @@ +/// +/// @file ringbuffer.h @brief Ringbuffer module header file +/// +/// Copyright (c) 2009, 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// @addtogroup Ringbuffer +/// @{ + + ///< ring buffer typedef +typedef struct _ring_buffer_ RingBuffer; + + ///< create new ring buffer +extern RingBuffer *RingBufferNew(size_t); + + ///< free ring buffer +extern void RingBufferDel(RingBuffer *); + + /// write into ring buffer +extern size_t RingBufferWrite(RingBuffer *, const void *, size_t); + + /// get write pointer of ring buffer +extern size_t RingBufferGetWritePointer(RingBuffer *, void **); + + /// advance write pointer of ring buffer +extern size_t RingBufferWriteAdvance(RingBuffer *, size_t); + + /// read from ring buffer +extern size_t RingBufferRead(RingBuffer *, void *, size_t); + + /// get read pointer of ring buffer +extern size_t RingBufferGetReadPointer(RingBuffer *, const void **); + + /// advance read pointer of ring buffer +extern size_t RingBufferReadAdvance(RingBuffer *, size_t); + + /// free bytes ring buffer +extern size_t RingBufferFreeBytes(RingBuffer *); + + /// used bytes ring buffer +extern size_t RingBufferUsedBytes(RingBuffer *); + +/// @} diff --git a/softhddev.c b/softhddev.c new file mode 100644 index 0000000..2bce10d --- /dev/null +++ b/softhddev.c @@ -0,0 +1,808 @@ +/// +/// @file softhddev.c @brief A software HD device plugin for VDR. +/// +/// Copyright (c) 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include + +#include +#include +#include + +#include +#define _(str) gettext(str) ///< gettext shortcut +#define _N(str) str ///< gettext_noop shortcut + +#include + +#include "misc.h" +#include "softhddev.h" + +#include "audio.h" +#include "video.h" +#include "codec.h" + +#define DEBUG + +////////////////////////////////////////////////////////////////////////////// +// Audio +////////////////////////////////////////////////////////////////////////////// + +static AudioDecoder *MyAudioDecoder; ///< audio decoder +static enum CodecID AudioCodecID; ///< current codec id + +extern void AudioTest(void); // FIXME: + +/** +** Play audio packet. +** +** @param data data of exactly one complete PES packet +** @param size size of PES packet +** @param id PES packet type +*/ +void PlayAudio(const uint8_t * data, int size, uint8_t id) +{ + int n; + AVPacket avpkt[1]; + + // PES header 0x00 0x00 0x01 ID + // ID 0xBD 0xC0-0xCF + + // channel switch: SetAudioChannelDevice: SetDigitalAudioDevice: + + // Detect audio code + // MPEG-PS mp2 MPEG1, MPEG2, AC3 + + if (size < 9) { + Error("[softhddev] invalid audio packet\n"); + return; + } + + avpkt->pts = AV_NOPTS_VALUE; + avpkt->dts = AV_NOPTS_VALUE; + if (data[7] & 0x80) { + avpkt->pts = + (int64_t) (data[9] & 0x0E) << 29 | data[10] << 22 | (data[11] & + 0xFE) << 15 | data[12] << 7 | (data[13] & 0xFE) >> 1; + // Debug(3, "audio: pts %ld\n", avpkt->pts); + } + if (data[7] & 0x40) { + avpkt->dts = + (int64_t) (data[14] & 0x0E) << 29 | data[15] << 22 | (data[16] & + 0xFE) << 15 | data[17] << 7 | (data[18] & 0xFE) >> 1; + Debug(3, "audio: dts %ld\n", avpkt->dts); + } + + n = data[8]; // header size + data += 9 + n; + size -= 9 + n; // skip pes header + if (size <= 0) { + Error("[softhddev] invalid audio packet\n"); + return; + } + // Syncword - 0x0B77 + if (data[0] == 0x0B && data[1] == 0x77) { + if (!MyAudioDecoder) { + MyAudioDecoder = CodecAudioNewDecoder(); + AudioCodecID = CODEC_ID_NONE; + } + if (AudioCodecID != CODEC_ID_AC3) { + Debug(3, "[softhddev]%s: AC-3 %d\n", __FUNCTION__, id); + CodecAudioClose(MyAudioDecoder); + CodecAudioOpen(MyAudioDecoder, NULL, CODEC_ID_AC3); + AudioCodecID = CODEC_ID_AC3; + } + // Syncword - 0xFFFC - 0xFFFF + } else if (data[0] == 0xFF && (data[1] & 0xFC) == 0xFC) { + if (!MyAudioDecoder) { + MyAudioDecoder = CodecAudioNewDecoder(); + AudioCodecID = CODEC_ID_NONE; + } + if (AudioCodecID != CODEC_ID_MP2) { + Debug(3, "[softhddev]%s: MP2 %d\n", __FUNCTION__, id); + CodecAudioClose(MyAudioDecoder); + CodecAudioOpen(MyAudioDecoder, NULL, CODEC_ID_MP2); + AudioCodecID = CODEC_ID_MP2; + } + } else { + // no start package + // FIXME: Nick/Viva sends this shit, need to find sync in packet + // FIXME: otherwise it takes too long until sound appears + if (AudioCodecID == CODEC_ID_NONE) { + Debug(3, "[softhddev]%s: ??? %d\n", __FUNCTION__, id); + return; + } + } + + // no decoder or codec known + if (!MyAudioDecoder || AudioCodecID == CODEC_ID_NONE) { + return; + } + + av_init_packet(avpkt); + avpkt->data = (void *)data; + avpkt->size = size; + //memset(avpkt->data + avpkt->size, 0, FF_INPUT_BUFFER_PADDING_SIZE); + CodecAudioDecode(MyAudioDecoder, avpkt); +} + +/** +** Mute audio device. +*/ +void Mute(void) +{ + AudioSetVolume(0); +} + +/** +** Set volume of audio device. +** +** @param volume VDR volume (0 .. 255) +*/ +void SetVolumeDevice(int volume) +{ + AudioSetVolume((volume * 100) / 255); +} + +////////////////////////////////////////////////////////////////////////////// +// Video +////////////////////////////////////////////////////////////////////////////// + +#include // portable atomic_t + +uint32_t VideoSwitch; +static int NewVideoStream; ///< new video stream +static VideoDecoder *MyVideoDecoder; ///< video decoder +static enum CodecID VideoCodecID; ///< current codec id + +static const char *X11DisplayName; ///< x11 display name +static volatile int Usr1Signal; ///< true got usr1 signal + + /// video PES buffer default size +#define VIDEO_BUFFER_SIZE (512 * 1024) +#define VIDEO_PACKET_MAX 128 ///< max number of video packets + /// video PES packet ring buffer +static AVPacket VideoPacketRb[VIDEO_PACKET_MAX]; +static int VideoPacketWrite; ///< write pointer +static int VideoPacketRead; ///< read pointer +static atomic_t VideoPacketsFilled; ///< how many of the buffer is used +static int VideoMaxPacketSize; ///< biggest used packet buffer +static uint32_t VideoStartTick; ///< video start tick + +extern void VideoWakeup(void); ///< wakeup video handler + +/** +** Initialize video packet ringbuffer. +*/ +static void VideoPacketInit(void) +{ + int i; + AVPacket *avpkt; + + Debug(4, "[softhddev]: %s\n", __FUNCTION__); + + for (i = 0; i < VIDEO_PACKET_MAX; ++i) { + avpkt = &VideoPacketRb[i]; + // build a clean ffmpeg av packet + av_init_packet(avpkt); + avpkt->destruct = av_destruct_packet; + avpkt->data = av_malloc(VIDEO_BUFFER_SIZE); + if (!avpkt->data) { + Fatal(_("[softhddev]: out of memory\n")); + } + avpkt->size = VIDEO_BUFFER_SIZE; + avpkt->priv = NULL; + } + + atomic_set(&VideoPacketsFilled, 0); +} + +/** +** Place video data in packet ringbuffer. +*/ +static void VideoEnqueue(const void *data, int size) +{ + AVPacket *avpkt; + + // Debug(3, "video: enqueue %d\n", size); + + avpkt = &VideoPacketRb[VideoPacketWrite]; + if (avpkt->stream_index + size + FF_INPUT_BUFFER_PADDING_SIZE >= + avpkt->size) { + + Warning(_("video: packet buffer too small for %d\n"), + avpkt->stream_index + size + FF_INPUT_BUFFER_PADDING_SIZE); + + av_grow_packet(avpkt, + (size + FF_INPUT_BUFFER_PADDING_SIZE + VIDEO_BUFFER_SIZE / 2) + / (VIDEO_BUFFER_SIZE / 2)); + } +#ifdef DEBUG + if (!avpkt->stream_index) { // debug save time of first packet + avpkt->dts = GetMsTicks(); + } +#endif + if (!VideoStartTick) { // tick of first valid packet + VideoStartTick = GetMsTicks(); + } + + memcpy(avpkt->data + avpkt->stream_index, data, size); + avpkt->stream_index += size; + if (avpkt->stream_index > VideoMaxPacketSize) { + VideoMaxPacketSize = avpkt->stream_index; + Debug(3, "video: max used PES packet size: %d\n", VideoMaxPacketSize); + } +} + +/** +** Finish current packet advance to next. +*/ +static void VideoNextPacket(int codec_id) +{ + AVPacket *avpkt; + + avpkt = &VideoPacketRb[VideoPacketWrite]; + if (!avpkt->stream_index) { // ignore empty packets + if (codec_id == CODEC_ID_NONE) { + Debug(3, "video: possible stream change loss\n"); + } + return; + } + + if (atomic_read(&VideoPacketsFilled) >= VIDEO_PACKET_MAX - 1) { + // no free slot available drop last packet + Error(_("video: no empty slot in packet ringbuffer\n")); + avpkt->stream_index = 0; + if (codec_id == CODEC_ID_NONE) { + Debug(3, "video: possible stream change loss\n"); + } + return; + } + // clear area for decoder, always enough space allocated + memset(avpkt->data + avpkt->stream_index, 0, FF_INPUT_BUFFER_PADDING_SIZE); + avpkt->priv = (void *)(size_t) codec_id; + + // advance packet write + VideoPacketWrite = (VideoPacketWrite + 1) % VIDEO_PACKET_MAX; + atomic_inc(&VideoPacketsFilled); + + // intialize next package to use + avpkt = &VideoPacketRb[VideoPacketWrite]; + avpkt->stream_index = 0; + avpkt->pts = AV_NOPTS_VALUE; + avpkt->dts = AV_NOPTS_VALUE; + + VideoWakeup(); +} + +/** +** Decode from PES packet ringbuffer. +*/ +int VideoDecode(void) +{ + int filled; + AVPacket *avpkt; + int saved_size; + static int last_codec_id = CODEC_ID_NONE; + + filled = atomic_read(&VideoPacketsFilled); + //Debug(3, "video: decode %3d packets buffered\n", filled); + if (!filled) { + // Debug(3, "video: decode no packets buffered\n"); + return -1; + } + avpkt = &VideoPacketRb[VideoPacketRead]; + + // + // handle queued commands + // + switch ((int)(size_t) avpkt->priv) { + case CODEC_ID_NONE: + if (last_codec_id != CODEC_ID_NONE) { + last_codec_id = CODEC_ID_NONE; + CodecVideoClose(MyVideoDecoder); + goto skip; + } + break; + case CODEC_ID_MPEG2VIDEO: + if (last_codec_id != CODEC_ID_MPEG2VIDEO) { + last_codec_id = CODEC_ID_MPEG2VIDEO; + CodecVideoOpen(MyVideoDecoder, 0 ? "mpegvideo_vdpau" : NULL, + CODEC_ID_MPEG2VIDEO); + } + break; + case CODEC_ID_H264: + if (last_codec_id != CODEC_ID_H264) { + last_codec_id = CODEC_ID_H264; + CodecVideoOpen(MyVideoDecoder, 0 ? "h264video_vdpau" : NULL, + CODEC_ID_H264); + } + break; + default: + break; + } + + // avcodec_decode_video2 needs size + saved_size = avpkt->size; + avpkt->size = avpkt->stream_index; + avpkt->stream_index = 0; + + CodecVideoDecode(MyVideoDecoder, avpkt); + + avpkt->size = saved_size; + + skip: + // advance packet read + VideoPacketRead = (VideoPacketRead + 1) % VIDEO_PACKET_MAX; + atomic_dec(&VideoPacketsFilled); + + return 0; +} + +/** +** Flush video buffer. +*/ +void VideoFlushInput(void) +{ + // flush all buffered packets + while (atomic_read(&VideoPacketsFilled)) { + VideoPacketRead = (VideoPacketRead + 1) % VIDEO_PACKET_MAX; + atomic_dec(&VideoPacketsFilled); + } + VideoStartTick = 0; +} + +/** +** Wakeup video handler. +*/ +void VideoWakeup(void) +{ + int filled; + uint32_t now; + uint64_t delay; + + VideoDisplayHandler(); + return; + + filled = atomic_read(&VideoPacketsFilled); + if (!filled) { + Debug(3, "video: wakeup no packets buffered\n"); + return; + } + + now = GetMsTicks(); + if (filled < VIDEO_PACKET_MAX && VideoStartTick + 1000 > now) { + delay = AudioGetDelay() / 90; + if (delay < 100) { // no audio delay known + delay = 750; + } + delay -= 40; + if (VideoStartTick + delay > now) { + Debug(3, "video: %d packets %u/%lu delayed\n", filled, + (unsigned)(now - VideoStartTick), delay); + return; + } + } + + VideoDecode(); + +#if 0 + AVPacket *avpkt; + + while (filled) { + avpkt = &VideoPacketRb[VideoPacketRead]; + now = GetMsTicks(); + if (avpkt->dts + 500 > now) { + Debug(3, "video: %d packets %u delayed\n", filled, + (unsigned)(now - avpkt->dts)); + return; + } + filled = atomic_read(&VideoPacketsFilled); + } +#endif +} + +/** +** Try video start. +** +** Could be called, when already started. +*/ +static void StartVideo(void) +{ + VideoInit(X11DisplayName); + VideoOsdInit(); + if (!MyVideoDecoder) { + VideoHwDecoder *hw_decoder; + + if ((hw_decoder = VideoNewHwDecoder())) { + MyVideoDecoder = CodecVideoNewDecoder(hw_decoder); + VideoCodecID = CODEC_ID_NONE; + } + } + VideoPacketInit(); +} + +/** +** Play video packet. +** +** @param data data of exactly one complete PES packet +** @param size size of PES packet +** +** @note vdr sends incomplete packets, va-api h264 decoder only +** supports complete packets. +** We buffer here until we receive an complete PES Packet, which +** is no problem, the audio is always far behind us. +*/ +void PlayVideo(const uint8_t * data, int size) +{ + const uint8_t *check; + uint64_t pts; + int n; + + if (Usr1Signal) { // x11 server ready + Usr1Signal = 0; + StartVideo(); + } + if (!MyVideoDecoder) { // no x11 video started + return; + } + if (NewVideoStream) { + Debug(3, "video: new stream %d\n", GetMsTicks() - VideoSwitch); + VideoNextPacket(CODEC_ID_NONE); + VideoCodecID = CODEC_ID_NONE; + NewVideoStream = 0; + } + // must be a PES start code + if (data[0] || data[1] || data[2] != 0x01 || size < 9) { + Error(_("[softhddev] invalid PES video packet\n")); + return; + } + n = data[8]; // header size + // wrong size + if (size < 9 + n) { + Error(_("[softhddev] invalid video packet\n")); + return; + } + check = data + 9 + n; + + // FIXME: get pts/dts, when we need it + + pts = AV_NOPTS_VALUE; + if (data[7] & 0x80) { + pts = + (int64_t) (data[9] & 0x0E) << 29 | data[10] << 22 | (data[11] & + 0xFE) << 15 | data[12] << 7 | (data[13] & 0xFE) >> 1; + // Debug(3, "video: pts %ld\n", pts); + } + // FIXME: no valid mpeg2/h264 detection yet + + if (0) { + printf("%02x: %02x %02x %02x %02x %02x\n", data[6], check[0], check[1], + check[2], check[3], check[4]); + } + // PES_VIDEO_STREAM 0xE0 or PES start code + if ((data[6] & 0xC0) != 0x80 || (!check[0] && !check[1] + && check[2] == 0x1)) { + if (VideoCodecID == CODEC_ID_MPEG2VIDEO) { + VideoNextPacket(CODEC_ID_MPEG2VIDEO); + } else { + VideoCodecID = CODEC_ID_MPEG2VIDEO; + } + // Access Unit Delimiter + } else if (!check[0] && !check[1] && !check[2] && check[3] == 0x1 + && check[4] == 0x09) { + if (VideoCodecID == CODEC_ID_H264) { + VideoNextPacket(CODEC_ID_H264); + } else { + Debug(3, "video: h264 detected\n"); + VideoCodecID = CODEC_ID_H264; + } + } else { + // this happens when vdr sends incomplete packets + if (VideoCodecID == CODEC_ID_NONE) { + Debug(3, "video: not detected\n"); + return; + } + if (VideoCodecID == CODEC_ID_MPEG2VIDEO) { + // mpeg codec supports incomplete packages + VideoNextPacket(CODEC_ID_MPEG2VIDEO); + } + } + + // SKIP PES header + size -= 9 + n; + VideoEnqueue(check, size); +} + +////////////////////////////////////////////////////////////////////////////// + +/** +** Set play mode, called on channel switch. +*/ +void SetPlayMode(void) +{ + if (MyVideoDecoder) { + if (VideoCodecID != CODEC_ID_NONE) { + NewVideoStream = 1; + VideoSwitch = GetMsTicks(); + } + } + if (MyAudioDecoder) { + // FIXME: does this clear the audio ringbuffer? + CodecAudioClose(MyAudioDecoder); + AudioCodecID = CODEC_ID_NONE; + } +} + +////////////////////////////////////////////////////////////////////////////// +// OSD +////////////////////////////////////////////////////////////////////////////// + +/** +** Get OSD size and aspect. +*/ +void GetOsdSize(int *width, int *height, double *aspect) +{ + static char done; + + // FIXME: should be configured! + *width = 1920; + *height = 1080; + //*width = 768; + //*height = 576; + + *aspect = 16.0 / 9.0 / (double)*width * (double)*height; + + if (!done) { + Debug(3, "[softhddev]%s: %dx%d %g\n", __FUNCTION__, *width, *height, + *aspect); + done = 1; + } +} + +/** +** Close OSD. +*/ +void OsdClose(void) +{ + VideoOsdClear(); +} + +/** +** Draw an OSD pixmap. +*/ +void OsdDrawARGB(int x, int y, int height, int width, const uint8_t * argb) +{ + VideoOsdDrawARGB(x, y, height, width, argb); +} + +////////////////////////////////////////////////////////////////////////////// + +static char StartX11Server; ///< flag start the x11 server + +/** +** Return command line help string. +*/ +const char *CommandLineHelp(void) +{ + return " -a device\talsa audio device (fe. hw:0,0)\n" + " -d display\tdisplay of x11 server (f.e :0.0)\n" + " -g geometry\tx11 window geometry wxh+x+y\n" + " -x\tstart x11 server\n"; +} + +/** +** Process the command line arguments. +** +** @param argc number of arguments +** @param argv arguments vector +*/ +int ProcessArgs(int argc, char *const argv[]) +{ + // + // Parse arguments. + // + for (;;) { + switch (getopt(argc, argv, "-a:d:g:x")) { + case 'a': // audio device + AudioSetDevice(optarg); + continue; + case 'd': // x11 display name + X11DisplayName = optarg; + continue; + case 'g': // geometry + if (VideoSetGeometry(optarg) < 0) { + fprintf(stderr, + _ + ("Bad formated geometry please use: [=][{xX}][{+-}{+-}]\n")); + return 0; + } + continue; + case 'x': // x11 server + StartX11Server = 1; + continue; + case EOF: + break; + case '-': + fprintf(stderr, _("We need no long options\n")); + return 0; + case ':': + fprintf(stderr, _("Missing argument for option '%c'\n"), + optopt); + return 0; + default: + fprintf(stderr, _("Unkown option '%c'\n"), optopt); + return 0; + } + break; + } + while (optind < argc) { + fprintf(stderr, _("Unhandled argument '%s'\n"), argv[optind++]); + } + + return 1; +} + +////////////////////////////////////////////////////////////////////////////// +// Init/Exit +////////////////////////////////////////////////////////////////////////////// + +#include +#include + +#define XSERVER_MAX_ARGS 512 ///< how many arguments support + +static const char *X11Server = "/usr/bin/X"; ///< default x11 server +static const char *X11ServerArguments; ///< default command arguments +static pid_t X11ServerPid; ///< x11 server pid + +/** +** USR1 signal handler. +** +** @param sig signal number +*/ +static void Usr1Handler(int __attribute__ ((unused)) sig) +{ + ++Usr1Signal; + + Debug(3, "x-setup: got signal usr1\n"); +} + +/** +** Start the X server +*/ +static void StartXServer(void) +{ + struct sigaction usr1; + pid_t pid; + const char *sval; + const char *args[XSERVER_MAX_ARGS]; + int argn; + char *buf; + + // X server + if (X11Server) { + args[0] = X11Server; + } else { + Error(_("x-setup: No X server configured!\n")); + return; + } + + argn = 1; + if (X11DisplayName) { // append display name + args[argn++] = X11DisplayName; + } + // split X server arguments string into words + if ((sval = X11ServerArguments)) { + char *s; + + s = buf = strdupa(sval); + while ((sval = strsep(&s, " \t"))) { + args[argn++] = sval; + + if (argn == XSERVER_MAX_ARGS - 1) { // argument overflow + Error(_("x-setup: too many arguments for xserver\n")); + // argn = 1; + break; + } + } + } + // FIXME: auth + // FIXME: append VTxx + args[argn] = NULL; + + // arm the signal + memset(&usr1, 0, sizeof(struct sigaction)); + usr1.sa_handler = Usr1Handler; + sigaction(SIGUSR1, &usr1, NULL); + + Debug(3, "x-setup: Starting X server '%s' '%s'\n", args[0], + X11ServerArguments); + // fork + if ((pid = vfork())) { // parent + + X11ServerPid = pid; + Debug(3, "x-setup: Started x-server pid=%d\n", X11ServerPid); + + return; + } + // child + signal(SIGUSR1, SIG_IGN); // ignore to force answer + // start the X server + execvp(args[0], (char *const *)args); + + Error(_("x-setup: Failed to start X server '%s'\n"), args[0]); +} + +/** +** Prepare plugin. +*/ +void Start(void) +{ + if (StartX11Server) { + StartXServer(); + } + CodecInit(); + // FIXME: AudioInit for HDMI after X11 startup + AudioInit(); + if (!StartX11Server) { + StartVideo(); + } +} + +/** +** Stop plugin. +*/ +void Stop(void) +{ + Debug(3, "video: max used PES packet size: %d\n", VideoMaxPacketSize); + + // lets hope that vdr does a good thead cleanup + if (MyVideoDecoder) { + CodecVideoClose(MyVideoDecoder); + MyVideoDecoder = NULL; + } + if (MyAudioDecoder) { + CodecAudioClose(MyAudioDecoder); + MyAudioDecoder = NULL; + } + + VideoExit(); + AudioExit(); + CodecExit(); + + if (StartX11Server) { + Debug(3, "x-setup: Stop x11 server\n"); + + if (X11ServerPid) { + kill(X11ServerPid, SIGTERM); + } + } +} + +/** +** Main thread hook, periodic called from main thread. +*/ +void MainThreadHook(void) +{ + VideoDisplayHandler(); +} diff --git a/softhddev.h b/softhddev.h new file mode 100644 index 0000000..34a87cd --- /dev/null +++ b/softhddev.h @@ -0,0 +1,67 @@ +/// +/// @file softhddev.h @brief software HD device plugin header file. +/// +/// Copyright (c) 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +#ifdef __cplusplus +extern "C" +{ +#endif + /// C callback feed key press + extern void FeedKeyPress(const char *, const char *, int, int); + + /// C plugin get osd size and ascpect + extern void GetOsdSize(int *, int *, double *); + + /// C plugin close osd + extern void OsdClose(void); + /// C plugin draw osd pixmap + extern void OsdDrawARGB(int, int, int, int, const uint8_t *); + + /// C plugin play audio packet + extern void PlayAudio(const uint8_t *, int, uint8_t); + /// C plugin mute audio + extern void Mute(void); + /// C plugin set audio volume + extern void SetVolumeDevice(int); + + /// C plugin play video packet + extern void PlayVideo(const uint8_t *, int); + /// C plugin play TS video packet + extern void PlayTsVideo(const uint8_t *, int); + + /// C plugin set play mode + extern void SetPlayMode(void); + + /// C plugin command line help + extern const char *CommandLineHelp(void); + /// C plugin process the command line arguments + extern int ProcessArgs(int, char *const[]); + + /// C plugin start code + extern void Start(void); + /// C plugin stop code + extern void Stop(void); + /// C plugin main thread hook + extern void MainThreadHook(void); + +#ifdef __cplusplus +} +#endif diff --git a/video.c b/video.c new file mode 100644 index 0000000..9bab2dc --- /dev/null +++ b/video.c @@ -0,0 +1,3827 @@ +/// +/// @file video.c @brief Video module +/// +/// Copyright (c) 2009 - 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// +/// @defgroup Video The video module. +/// +/// This module contains all video rendering functions. +/// +/// @todo hide mouse cursor support +/// +/// Uses Xlib where it is needed for VA-API or vdpau. XCB is used for +/// everything else. +/// +/// - X11 +/// - OpenGL rendering +/// - OpenGL rendering with GLX texture-from-pixmap +/// - Xrender rendering +/// + +#define DEBUG +#define USE_XLIB_XCB +#define noUSE_GLX +#define noUSE_DOUBLEBUFFER + +#define USE_VAAPI +#define noUSE_VDPAU +#define noUSE_BITMAP + +#define USE_VIDEO_THREAD + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#define _(str) gettext(str) ///< gettext shortcut +#define _N(str) str ///< gettext_noop shortcut + +#include // portable atomic_t + +#ifdef USE_VIDEO_THREAD +#ifndef __USE_GNU +#define __USE_GNU +#endif +#include +#include +#endif + +#ifdef USE_XLIB_XCB +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#endif + +#ifdef USE_GLX +#include +// only for gluErrorString +#include +#endif + +#ifdef USE_VAAPI +#include +#ifdef USE_GLX +#include +#endif +#endif + +#ifdef USE_VDPAU +#include +#endif + +#include +#include +#include + +#include "misc.h" +#include "video.h" +#include "audio.h" + +#ifdef USE_XLIB_XCB + +//---------------------------------------------------------------------------- +// Declarations +//---------------------------------------------------------------------------- + +/// +/// Video deinterlace modes. +/// +typedef enum _video_deinterlace_modes_ +{ + VideoDeinterlaceBob, ///< bob deinterlace + VideoDeinterlaceWeave, ///< weave deinterlace + VideoDeinterlaceTemporal, ///< temporal deinterlace + VideoDeinterlaceTemporalSpatial, ///< temporal spatial deinterlace + VideoDeinterlaceSoftware, ///< software deinterlace +} VideoDeinterlaceModes; + +/// +/// Video scalinng modes. +/// +typedef enum _video_scaling_modes_ +{ + VideoScalingNormal, ///< normal scaling + VideoScalingFast, ///< fastest scaling + VideoScalingHQ, ///< high quality scaling + VideoScalingAnamorphic, ///< anamorphic scaling +} VideoScalingModes; + +//---------------------------------------------------------------------------- +// Defines +//---------------------------------------------------------------------------- + +#define CODEC_SURFACES_MAX 31 ///< maximal of surfaces +#define CODEC_SURFACES_DEFAULT (21+4) ///< default of surfaces +#define CODEC_SURFACES_MPEG2 3 ///< 1 decode, up to 2 references +#define CODEC_SURFACES_MPEG4 3 ///< 1 decode, up to 2 references +#define CODEC_SURFACES_H264 21 ///< 1 decode, up to 20 references +#define CODEC_SURFACES_VC1 3 ///< 1 decode, up to 2 references + +#define VIDEO_SURFACES_MAX 3 ///< video output surfaces for queue +#define OUTPUT_SURFACES_MAX 4 ///< output surfaces for flip page + +//---------------------------------------------------------------------------- +// Variables +//---------------------------------------------------------------------------- + +static Display *XlibDisplay; ///< Xlib X11 display +static xcb_connection_t *Connection; ///< xcb connection +static xcb_colormap_t VideoColormap; ///< video colormap +static xcb_window_t VideoWindow; ///< video window + +static int VideoWindowX; ///< video output x +static int VideoWindowY; ///< video outout y +static unsigned VideoWindowWidth; ///< video output width +static unsigned VideoWindowHeight; ///< video output height + + /// Default deinterlace mode +static VideoDeinterlaceModes VideoDeinterlace; + + /// Default scaling mode +static VideoScalingModes VideoScaling; + +static xcb_atom_t WmDeleteWindowAtom; ///< WM delete message + +extern uint32_t VideoSwitch; + +//---------------------------------------------------------------------------- +// Functions +//---------------------------------------------------------------------------- + +static void VideoThreadLock(void); ///< lock video thread +static void VideoThreadUnlock(void); ///< unlock video thread + +//---------------------------------------------------------------------------- +// GLX +//---------------------------------------------------------------------------- + +#ifdef USE_GLX + +static int GlxEnabled = 1; ///< use GLX +static int GlxVSyncEnabled = 0; ///< enable/disable v-sync +static GLXContext GlxSharedContext; ///< shared gl context +static GLXContext GlxContext; ///< our gl context +static XVisualInfo *GlxVisualInfo; ///< our gl visual + +static GLuint OsdGlTextures[2]; ///< gl texture for OSD +static int OsdIndex; ///< index into OsdGlTextures + +/// +/// GLX extension functions +///@{ +#ifdef GLX_MESA_swap_control +static PFNGLXSWAPINTERVALMESAPROC GlxSwapIntervalMESA; +#endif +#ifdef GLX_SGI_video_sync +static PFNGLXGETVIDEOSYNCSGIPROC GlxGetVideoSyncSGI; +#endif +#ifdef GLX_SGI_swap_control +static PFNGLXSWAPINTERVALSGIPROC GlxSwapIntervalSGI; +#endif + +///@} + +/// +/// GLX check error. +/// +static void GlxCheck(void) +{ + GLenum err; + + if ((err = glGetError()) != GL_NO_ERROR) { + Debug(3, "video/glx: error %d '%s'\n", err, gluErrorString(err)); + } +} + +/// +/// GLX check if a GLX extension is supported. +/// +/// @param ext extension to query +/// @returns true if supported, false otherwise +/// +static int GlxIsExtensionSupported(const char *ext) +{ + const char *extensions; + + if ((extensions = + glXQueryExtensionsString(XlibDisplay, + DefaultScreen(XlibDisplay)))) { + const char *s; + int l; + + s = strstr(extensions, ext); + l = strlen(ext); + return s && (s[l] == ' ' || s[l] == '\0'); + } + return 0; +} + +#if 0 +/// +/// Setup GLX decoder +/// +/// @param decoder VA-API decoder +/// +void GlxSetupDecoder(VaapiDecoder * decoder) +{ + int width; + int height; + int i; + + width = decoder->InputWidth; + height = decoder->InputHeight; + + glEnable(GL_TEXTURE_2D); // create 2d texture + glGenTextures(2, decoder->GlTexture); + GlxCheck(); + for (i = 0; i < 2; ++i) { + glBindTexture(GL_TEXTURE_2D, decoder->GlTexture[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_BGRA, + GL_UNSIGNED_BYTE, NULL); + glBindTexture(GL_TEXTURE_2D, 0); + } + glDisable(GL_TEXTURE_2D); + + GlxCheck(); +} +#endif + +/** +** Render texture. +** +** @param texture 2d texture +*/ +static inline void GlxRenderTexture(GLuint texture, int x, int y, int width, + int height) +{ + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, texture); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); // no color + glBegin(GL_QUADS); { + glTexCoord2f(1.0f, 1.0f); + glVertex2i(x + width, y + height); + glTexCoord2f(0.0f, 1.0f); + glVertex2i(x, y + height); + glTexCoord2f(0.0f, 0.0f); + glVertex2i(x, y); + glTexCoord2f(1.0f, 0.0f); + glVertex2i(x + width, y); +#if 0 + glTexCoord2f(0.0f, 0.0f); + glVertex2i(x, y); + glTexCoord2f(0.0f, 1.0f); + glVertex2i(x, y + height); + glTexCoord2f(1.0f, 1.0f); + glVertex2i(x + width, y + height); + glTexCoord2f(1.0f, 0.0f); + glVertex2i(x + width, y); +#endif + } + glEnd(); + + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); +} + +/** +** Upload texture. +*/ +static void GlxUploadTexture(int x, int y, int width, int height, + const uint8_t * argb) +{ + // FIXME: use other / faster uploads + // ARB_pixelbuffer_object GL_PIXEL_UNPACK_BUFFER glBindBufferARB() + // glMapBuffer() glUnmapBuffer() + // glTexSubImage2D + + glEnable(GL_TEXTURE_2D); // upload 2d texture + + glBindTexture(GL_TEXTURE_2D, OsdGlTextures[OsdIndex]); + glTexSubImage2D(GL_TEXTURE_2D, 0, x, y, width, height, GL_BGRA, + GL_UNSIGNED_BYTE, argb); + glBindTexture(GL_TEXTURE_2D, 0); + + glDisable(GL_TEXTURE_2D); +} + +/** +** Render to glx texture. +*/ +static void GlxRender(int osd_width, int osd_height) +{ + static uint8_t *image; + static uint8_t cycle; + int x; + int y; + + if (!OsdGlTextures[0] || !OsdGlTextures[1]) { + return; + } + // render each frame kills performance + + // osd 1920 * 1080 * 4 (RGBA) * 50 (HZ) = 396 Mb/s + + // too big for alloca + if (!image) { + image = malloc(4 * osd_width * osd_height); + memset(image, 0x00, 4 * osd_width * osd_height); + } + for (y = 0; y < osd_height; ++y) { + for (x = 0; x < osd_width; ++x) { + ((uint32_t *) image)[x + y * osd_width] = + 0x00FFFFFF | (cycle++) << 24; + } + } + cycle++; + + // FIXME: convert is for GLX texture unneeded + // convert internal osd to image + //GfxConvert(image, 0, 4 * osd_width); + // + + GlxUploadTexture(0, 0, osd_width, osd_height, image); +} + +/// +/// Setup GLX window. +/// +static void GlxSetupWindow(xcb_window_t window, int width, int height) +{ + uint32_t start; + uint32_t end; + int i; + unsigned count; + + Debug(3, "video/glx: %s\n %x %dx%d", __FUNCTION__, window, width, height); + + // set glx context + if (!glXMakeCurrent(XlibDisplay, window, GlxContext)) { + Fatal(_("video/glx: can't make glx context current\n")); + // FIXME: disable glx + return; + } + + Debug(3, "video/glx: ok\n"); + +#ifdef DEBUG + // check if v-sync is working correct + end = GetMsTicks(); + for (i = 0; i < 10; ++i) { + start = end; + + glClear(GL_COLOR_BUFFER_BIT); + glXSwapBuffers(XlibDisplay, window); + end = GetMsTicks(); + + GlxGetVideoSyncSGI(&count); + Debug(3, "video/glx: %5d frame rate %d ms\n", count, end - start); + // nvidia can queue 5 swaps + if (i > 5 && (end - start) < 15) { + Warning(_("video/glx: no v-sync\n")); + } + } +#endif + + // viewpoint + GlxCheck(); + glViewport(0, 0, width, height); + glDepthRange(-1.0, 1.0); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + glColor3f(1.0f, 1.0f, 1.0f); + glClearDepth(1.0); + GlxCheck(); + + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0.0, width, height, 0.0, -1.0, 1.0); + GlxCheck(); + + glMatrixMode(GL_MODELVIEW); + glLoadIdentity(); + + glDisable(GL_DEPTH_TEST); // setup 2d drawing + glDepthMask(GL_FALSE); + glDisable(GL_CULL_FACE); +#ifdef USE_DOUBLEBUFFER + glDrawBuffer(GL_BACK); +#else + glDrawBuffer(GL_FRONT); +#endif + glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + +#ifdef DEBUG +#ifdef USE_DOUBLEBUFFER + glDrawBuffer(GL_FRONT); + glClearColor(1.0f, 0.0f, 1.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + glDrawBuffer(GL_BACK); +#endif +#endif + + // clear + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); // intial background color + glClear(GL_COLOR_BUFFER_BIT); +#ifdef DEBUG + glClearColor(1.0f, 1.0f, 0.0f, 1.0f); // background color +#endif + GlxCheck(); +} + +/// +/// Initialize GLX. +/// +static void GlxInit(void) +{ + static GLint visual_attr[] = { + GLX_RGBA, + GLX_RED_SIZE, 8, + GLX_GREEN_SIZE, 8, + GLX_BLUE_SIZE, 8, +#ifdef USE_DOUBLEBUFFER + GLX_DOUBLEBUFFER, +#endif + None + }; + XVisualInfo *vi; + GLXContext context; + int major; + int minor; + int glx_GLX_EXT_swap_control; + int glx_GLX_MESA_swap_control; + int glx_GLX_SGI_swap_control; + int glx_GLX_SGI_video_sync; + + if (!glXQueryVersion(XlibDisplay, &major, &minor)) { + Error(_("video/glx: no GLX support\n")); + GlxEnabled = 0; + return; + } + Info(_("video/glx: glx version %d.%d\n"), major, minor); + + // + // check which extension are supported + // + glx_GLX_EXT_swap_control = GlxIsExtensionSupported("GLX_EXT_swap_control"); + glx_GLX_MESA_swap_control = + GlxIsExtensionSupported("GLX_MESA_swap_control"); + glx_GLX_SGI_swap_control = GlxIsExtensionSupported("GLX_SGI_swap_control"); + glx_GLX_SGI_video_sync = GlxIsExtensionSupported("GLX_SGI_video_sync"); + +#ifdef GLX_MESA_swap_control + if (glx_GLX_MESA_swap_control) { + GlxSwapIntervalMESA = (PFNGLXSWAPINTERVALMESAPROC) + glXGetProcAddress((const GLubyte *)"glXSwapIntervalMESA"); + } + Debug(3, "video/glx: GlxSwapIntervalMESA=%p\n", GlxSwapIntervalMESA); +#endif +#ifdef GLX_SGI_swap_control + if (glx_GLX_SGI_swap_control) { + GlxSwapIntervalSGI = (PFNGLXSWAPINTERVALSGIPROC) + glXGetProcAddress((const GLubyte *)"glXSwapIntervalSGI"); + } + Debug(3, "video/glx: GlxSwapIntervalSGI=%p\n", GlxSwapIntervalSGI); +#endif +#ifdef GLX_SGI_video_sync + if (glx_GLX_SGI_video_sync) { + GlxGetVideoSyncSGI = (PFNGLXGETVIDEOSYNCSGIPROC) + glXGetProcAddress((const GLubyte *)"glXGetVideoSyncSGI"); + } + Debug(3, "video/glx: GlxGetVideoSyncSGI=%p\n", GlxGetVideoSyncSGI); +#endif + // glXGetVideoSyncSGI glXWaitVideoSyncSGI + +#if 0 + // FIXME: use xcb: xcb_glx_create_context +#endif + + // create glx context + glXMakeCurrent(XlibDisplay, None, NULL); + vi = glXChooseVisual(XlibDisplay, DefaultScreen(XlibDisplay), visual_attr); + if (!vi) { + Error(_("video/glx: can't get a RGB visual\n")); + GlxEnabled = 0; + return; + } + if (!vi->visual) { + Error(_("video/glx: no valid visual found\n")); + GlxEnabled = 0; + return; + } + if (vi->bits_per_rgb < 8) { + Error(_("video/glx: need atleast 8-bits per RGB\n")); + GlxEnabled = 0; + return; + } + context = glXCreateContext(XlibDisplay, vi, NULL, GL_TRUE); + if (!context) { + Error(_("video/glx: can't create glx context\n")); + GlxEnabled = 0; + return; + } + GlxSharedContext = context; + context = glXCreateContext(XlibDisplay, vi, GlxSharedContext, GL_TRUE); + if (!context) { + Error(_("video/glx: can't create glx context\n")); + GlxEnabled = 0; + // FIXME: destroy GlxSharedContext + return; + } + GlxContext = context; + + GlxVisualInfo = vi; + Debug(3, "video/glx: visual %#02x depth %u\n", (unsigned)vi->visualid, + vi->depth); + + // + // query default v-sync state + // + if (glx_GLX_EXT_swap_control) { + unsigned tmp; + + tmp = -1; + glXQueryDrawable(XlibDisplay, DefaultRootWindow(XlibDisplay), + GLX_SWAP_INTERVAL_EXT, &tmp); + GlxCheck(); + + Debug(3, "video/glx: default v-sync is %d\n", tmp); + } else { + Debug(3, "video/glx: default v-sync is unknown\n"); + } + + // + // disable wait on v-sync + // + // FIXME: sleep before swap / busy waiting hardware + // FIXME: 60hz lcd panel + // FIXME: config: default, on, off +#ifdef GLX_SGI_swap_control + if (GlxVSyncEnabled < 0 && GlxSwapIntervalSGI) { + if (GlxSwapIntervalSGI(0)) { + GlxCheck(); + Warning(_("video/glx: can't disable v-sync\n")); + } else { + Info(_("video/glx: v-sync disabled\n")); + } + } else +#endif +#ifdef GLX_MESA_swap_control + if (GlxVSyncEnabled < 0 && GlxSwapIntervalMESA) { + if (GlxSwapIntervalMESA(0)) { + GlxCheck(); + Warning(_("video/glx: can't disable v-sync\n")); + } else { + Info(_("video/glx: v-sync disabled\n")); + } + } +#endif + + // + // enable wait on v-sync + // +#ifdef GLX_SGI_swap_control + if (GlxVSyncEnabled > 0 && GlxSwapIntervalMESA) { + if (GlxSwapIntervalMESA(1)) { + GlxCheck(); + Warning(_("video/glx: can't enable v-sync\n")); + } else { + Info(_("video/glx: v-sync enabled\n")); + } + } else +#endif +#ifdef GLX_MESA_swap_control + if (GlxVSyncEnabled > 0 && GlxSwapIntervalSGI) { + if (GlxSwapIntervalSGI(1)) { + GlxCheck(); + Warning(_("video/glx: can't enable v-sync\n")); + } else { + Info(_("video/glx: v-sync enabled\n")); + } + } +#endif +} + +/// +/// Cleanup GLX. +/// +static void GlxExit(void) +{ + Debug(3, "video/glx: %s\n", __FUNCTION__); + + glFinish(); + + // must destroy glx + if (glXGetCurrentContext() == GlxContext) { + // if currently used, set to none + glXMakeCurrent(XlibDisplay, None, NULL); + } + if (GlxSharedContext) { + glXDestroyContext(XlibDisplay, GlxSharedContext); + } + if (GlxContext) { + glXDestroyContext(XlibDisplay, GlxContext); + } +#if 0 + if (GlxThreadContext) { + glXDestroyContext(XlibDisplay, GlxThreadContext); + } + // FIXME: must free GlxVisualInfo +#endif +} + +#endif + +//---------------------------------------------------------------------------- +// VA-API +//---------------------------------------------------------------------------- + +#ifdef USE_VAAPI + +static int VideoVaapiEnabled = 1; ///< use VA-API decoder +static int VaapiBuggyVdpau; ///< fix libva-driver-vdpau bugs + +static VADisplay *VaDisplay; ///< VA-API display + +static VAImage VaOsdImage = { + .image_id = VA_INVALID_ID +}; ///< osd VA-API image + +static VASubpictureID VaOsdSubpicture; ///< osd VA-API subpicture +static char VaapiUnscaledOsd; ///< unscaled osd supported + + /// VA-API decoder typedef +typedef struct _vaapi_decoder_ VaapiDecoder; + +/// +/// VA-API decoder +/// +struct _vaapi_decoder_ +{ + VADisplay *VaDisplay; ///< VA-API display + unsigned SurfaceFlags; ///< flags for put surface + + xcb_window_t Window; ///< output window + int OutputX; ///< output window x + int OutputY; ///< output window y + int OutputWidth; ///< output window width + int OutputHeight; ///< output window height + + enum PixelFormat PixFmt; ///< ffmpeg frame pixfmt + int WrongInterlacedWarned; ///< warning about interlace flag issued + int Interlaced; ///< ffmpeg interlaced flag + int TopFieldFirst; ///< ffmpeg top field displayed first + + VAImage DeintImages[3]; ///< deinterlace image buffers + + VAImage Image[1]; ///< image buffer to update surface + + struct vaapi_context VaapiContext[1]; ///< ffmpeg VA-API context + + int SurfaceUsedN; ///< number of used surfaces + /// used surface ids + VASurfaceID SurfacesUsed[CODEC_SURFACES_MAX]; + int SurfaceFreeN; ///< number of free surfaces + /// free surface ids + VASurfaceID SurfacesFree[CODEC_SURFACES_MAX]; + + int InputX; ///< input x + int InputY; ///< input y + int InputWidth; ///< input width + int InputHeight; ///< input height + AVRational InputAspect; ///< input aspect ratio + +#ifdef USE_GLX + GLuint GlTexture[2]; ///< gl texture for VA-API + void *GlxSurface[2]; ///< VA-API/GLX surface +#endif + VASurfaceID BlackSurface; ///< empty black surface + + /// video surface ring buffer + VASurfaceID SurfacesRb[VIDEO_SURFACES_MAX]; + int SurfaceWrite; ///< write pointer + int SurfaceRead; ///< read pointer + atomic_t SurfacesFilled; ///< how many of the buffer is used + + int SurfaceField; ///< current displayed field + struct timespec FrameTime; ///< time of last display + struct timespec StartTime; ///< decoder start time + + int FramesDuped; ///< frames duplicated + int FramesDropped; ///< frames dropped + int FrameCounter; ///< number of frames decoded +}; + +static VaapiDecoder *VaapiDecoders[1]; ///< open decoder streams +static int VaapiDecoderN; ///< number of decoder streams + + /// forward display back surface +static void VaapiBlackSurface(VaapiDecoder * decoder); + +//---------------------------------------------------------------------------- +// VA-API Functions +//---------------------------------------------------------------------------- + +// Surfaces ------------------------------------------------------------- + +/// +/// Create surfaces for VA-API decoder. +/// +/// @param decoder VA-API decoder +/// @param width surface source/video width +/// @param height surface source/video height +/// +static void VaapiCreateSurfaces(VaapiDecoder * decoder, int width, int height) +{ + Debug(3, "video/vaapi: %s: %dx%d * %d\n", __FUNCTION__, width, height, + CODEC_SURFACES_DEFAULT); + + // FIXME: allocate only the number of needed surfaces + decoder->SurfaceFreeN = CODEC_SURFACES_DEFAULT; + // VA_RT_FORMAT_YUV420 VA_RT_FORMAT_YUV422 VA_RT_FORMAT_YUV444 + if (vaCreateSurfaces(decoder->VaDisplay, width, height, + VA_RT_FORMAT_YUV420, decoder->SurfaceFreeN, + decoder->SurfacesFree) != VA_STATUS_SUCCESS) { + Fatal(_("video/vaapi: can't create %d surfaces\n"), + decoder->SurfaceFreeN); + // FIXME: write error handler / fallback + } + // + // update OSD associate + // + if (VaOsdSubpicture == VA_INVALID_ID) { + Warning(_("video/vaapi: no osd subpicture yet\n")); + return; + } + + if (VaapiUnscaledOsd) { + if (vaAssociateSubpicture(VaDisplay, VaOsdSubpicture, + decoder->SurfacesFree, decoder->SurfaceFreeN, 0, 0, + VaOsdImage.width, VaOsdImage.height, 0, 0, VideoWindowWidth, + VideoWindowHeight, VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't associate subpicture\n")); + } + } else { + if (vaAssociateSubpicture(VaDisplay, VaOsdSubpicture, + decoder->SurfacesFree, decoder->SurfaceFreeN, 0, 0, + VaOsdImage.width, VaOsdImage.height, 0, 0, width, height, 0) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't associate subpicture\n")); + } + } +} + +/// +/// Destroy surfaces of VA-API decoder. +/// +/// @param decoder VA-API decoder +/// +static void VaapiDestroySurfaces(VaapiDecoder * decoder) +{ + Debug(3, "video/vaapi: %s:\n", __FUNCTION__); + + // + // update OSD associate + // + if (VaOsdSubpicture != VA_INVALID_ID) { + if (vaDeassociateSubpicture(VaDisplay, VaOsdSubpicture, + decoder->SurfacesFree, decoder->SurfaceFreeN) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't deassociate %d surfaces\n"), + decoder->SurfaceFreeN); + } + + if (vaDeassociateSubpicture(VaDisplay, VaOsdSubpicture, + decoder->SurfacesUsed, decoder->SurfaceUsedN) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't deassociate %d surfaces\n"), + decoder->SurfaceUsedN); + } + } + + if (vaDestroySurfaces(decoder->VaDisplay, decoder->SurfacesFree, + decoder->SurfaceFreeN) + != VA_STATUS_SUCCESS) { + Error("video/vaapi: can't destroy %d surfaces\n", + decoder->SurfaceFreeN); + } + decoder->SurfaceFreeN = 0; + if (vaDestroySurfaces(decoder->VaDisplay, decoder->SurfacesUsed, + decoder->SurfaceUsedN) + != VA_STATUS_SUCCESS) { + Error("video/vaapi: can't destroy %d surfaces\n", + decoder->SurfaceUsedN); + } + decoder->SurfaceUsedN = 0; + + // FIXME surfaces used for output +} + +/// +/// Get a free surface. +/// +/// @param decoder VA-API decoder +/// +static VASurfaceID VaapiGetSurface(VaapiDecoder * decoder) +{ + VASurfaceID surface; + int i; + + if (!decoder->SurfaceFreeN) { + Error("video/vaapi: out of surfaces\n"); + return VA_INVALID_ID; + } + // use oldest surface + surface = decoder->SurfacesFree[0]; + + decoder->SurfaceFreeN--; + for (i = 0; i < decoder->SurfaceFreeN; ++i) { + decoder->SurfacesFree[i] = decoder->SurfacesFree[i + 1]; + } + + // save as used + decoder->SurfacesUsed[decoder->SurfaceUsedN++] = surface; + + return surface; +} + +/// +/// Release a surface. +/// +/// @param decoder VA-API decoder +/// @param surface surface no longer used +/// +static void VaapiReleaseSurface(VaapiDecoder * decoder, VASurfaceID surface) +{ + int i; + + for (i = 0; i < decoder->SurfaceUsedN; ++i) { + if (decoder->SurfacesUsed[i] == surface) { + // no problem, with last used + decoder->SurfacesUsed[i] = + decoder->SurfacesUsed[--decoder->SurfaceUsedN]; + decoder->SurfacesFree[decoder->SurfaceFreeN++] = surface; + return; + } + } + Error(_("video/vaapi: release surface %#x, which is not in use\n"), + surface); +} + +// Init/Exit ------------------------------------------------------------ + +/// +/// Debug VA-API decoder frames drop... +/// +/// @param decoder video hardware decoder +/// +static void VaapiPrintFrames(const VaapiDecoder * decoder) +{ + Debug(3, "video/vaapi: %d duped, %d dropped frames of %d\n", + decoder->FramesDuped, decoder->FramesDropped, decoder->FrameCounter); +} + +/// +/// Allocate new VA-API decoder. +/// +static VaapiDecoder *VaapiNewDecoder(void) +{ + VaapiDecoder *decoder; + int i; + + if (VaapiDecoderN == 1) { + Fatal(_("video/vaapi: out of decoders\n")); + } + + if (!(decoder = calloc(1, sizeof(*decoder)))) { + Fatal(_("video/vaapi: out of memory\n")); + } + decoder->VaDisplay = VaDisplay; + decoder->Window = VideoWindow; + + decoder->SurfaceFlags = VA_CLEAR_DRAWABLE; + // color space conversion none, ITU-R BT.601, ITU-R BT.709 + decoder->SurfaceFlags |= VA_SRC_BT601; + + // scaling flags FAST, HQ, NL_ANAMORPHIC + // FIXME: need to detect the backend to choose the parameter + switch (VideoScaling) { + case VideoScalingNormal: + decoder->SurfaceFlags |= VA_FILTER_SCALING_DEFAULT; + break; + case VideoScalingFast: + decoder->SurfaceFlags |= VA_FILTER_SCALING_FAST; + break; + case VideoScalingHQ: + // vdpau backend supports only VA_FILTER_SCALING_HQ + // vdpau backend with advanced deinterlacer and my GT-210 + // is too slow + decoder->SurfaceFlags |= VA_FILTER_SCALING_HQ; + break; + case VideoScalingAnamorphic: + // intel backend supports only VA_FILTER_SCALING_NL_ANAMORPHIC; + // don't use it, its for 4:3 -> 16:9 scaling + decoder->SurfaceFlags |= VA_FILTER_SCALING_NL_ANAMORPHIC; + break; + } + + // deinterlace flags (not yet supported by libva) + switch (VideoDeinterlace) { + case VideoDeinterlaceBob: + break; + case VideoDeinterlaceWeave: + break; + case VideoDeinterlaceTemporal: + //FIXME: private hack + //decoder->SurfaceFlags |= 0x00002000; + break; + case VideoDeinterlaceTemporalSpatial: + //FIXME: private hack + //decoder->SurfaceFlags |= 0x00006000; + break; + case VideoDeinterlaceSoftware: + break; + } + + decoder->DeintImages[0].image_id = VA_INVALID_ID; + decoder->DeintImages[1].image_id = VA_INVALID_ID; + decoder->DeintImages[2].image_id = VA_INVALID_ID; + + decoder->Image->image_id = VA_INVALID_ID; + + // setup video surface ring buffer + atomic_set(&decoder->SurfacesFilled, 0); + + for (i = 0; i < VIDEO_SURFACES_MAX; ++i) { + decoder->SurfacesRb[i] = VA_INVALID_ID; + } + + decoder->BlackSurface = VA_INVALID_ID; + + // + // Setup ffmpeg vaapi context + // + decoder->VaapiContext->display = VaDisplay; + decoder->VaapiContext->config_id = VA_INVALID_ID; + decoder->VaapiContext->context_id = VA_INVALID_ID; + +#ifdef USE_GLX + if (GlxEnabled) { + // FIXME: create GLX context here + } +#endif + + decoder->OutputWidth = VideoWindowWidth; + decoder->OutputHeight = VideoWindowHeight; + +#ifdef noDEBUG + // FIXME: for play + decoder->OutputX = 40; + decoder->OutputY = 40; + decoder->OutputWidth = VideoWindowWidth - 40 * 2; + decoder->OutputHeight = VideoWindowHeight - 40 * 2; +#endif + + VaapiDecoders[VaapiDecoderN++] = decoder; + + return decoder; +} + +/** +** Cleanup VA-API. +** +** @param decoder va-api hw decoder +*/ +static void VaapiCleanup(VaapiDecoder * decoder) +{ + int filled; + VASurfaceID surface; + + // flush output queue, only 1-2 frames buffered, no big loss + while ((filled = atomic_read(&decoder->SurfacesFilled))) { + decoder->SurfaceRead = (decoder->SurfaceRead + 1) % VIDEO_SURFACES_MAX; + atomic_dec(&decoder->SurfacesFilled); + + surface = decoder->SurfacesRb[decoder->SurfaceRead]; + if (vaSyncSurface(decoder->VaDisplay, surface) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + } + + decoder->WrongInterlacedWarned = 0; + + // cleanup image + if (decoder->Image->image_id != VA_INVALID_ID) { + if (vaDestroyImage(VaDisplay, + decoder->Image->image_id) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy image!\n")); + } + decoder->Image->image_id = VA_INVALID_ID; + } + // cleanup context and config + if (decoder->VaapiContext) { + + if (decoder->VaapiContext->context_id != VA_INVALID_ID) { + if (vaDestroyContext(VaDisplay, + decoder->VaapiContext->context_id) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy context!\n")); + } + decoder->VaapiContext->context_id = VA_INVALID_ID; + } + + if (decoder->VaapiContext->config_id != VA_INVALID_ID) { + if (vaDestroyConfig(VaDisplay, + decoder->VaapiContext->config_id) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy config!\n")); + } + decoder->VaapiContext->config_id = VA_INVALID_ID; + } + } + // cleanup surfaces + if (decoder->SurfaceFreeN || decoder->SurfaceUsedN) { + VaapiDestroySurfaces(decoder); + } + + clock_gettime(CLOCK_REALTIME, &decoder->StartTime); +} + +/// +/// Destroy a VA-API decoder. +/// +/// @param decoder VA-API decoder +/// +static void VaapiDelDecoder(VaapiDecoder * decoder) +{ + VaapiCleanup(decoder); + + if (decoder->BlackSurface) { + if (vaDestroySurfaces(decoder->VaDisplay, &decoder->BlackSurface, 1) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy a surface\n")); + } + } + // FIXME: decoder->DeintImages +#ifdef USE_GLX + if (decoder->GlxSurface[0]) { + if (vaDestroySurfaceGLX(VaDisplay, decoder->GlxSurface[0]) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy glx surface!\n")); + } + } + if (decoder->GlxSurface[1]) { + if (vaDestroySurfaceGLX(VaDisplay, decoder->GlxSurface[1]) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy glx surface!\n")); + } + } + if (decoder->GlTexture[0]) { + glDeleteTextures(2, decoder->GlTexture); + } +#endif + + VaapiPrintFrames(decoder); + + free(decoder); +} + +/** +** VA-API setup. +** +** @param display_name x11/xcb display name +*/ +static void VideoVaapiInit(const char *display_name) +{ + int major; + int minor; + VADisplayAttribute attr; + const char *s; + + // FIXME: make configurable + // FIXME: intel get hangups with bob + // VideoDeinterlace = VideoDeinterlaceWeave; + + VaOsdImage.image_id = VA_INVALID_ID; + VaOsdSubpicture = VA_INVALID_ID; + +#ifdef USE_GLX + if (GlxEnabled) { // support glx + VaDisplay = vaGetDisplayGLX(XlibDisplay); + } else +#endif + { + VaDisplay = vaGetDisplay(XlibDisplay); + } + if (!VaDisplay) { + Fatal(_("video/vaapi: Can't connect VA-API to X11 server on '%s'"), + display_name); + // FIXME: no fatal for plugin + } + + if (vaInitialize(VaDisplay, &major, &minor) != VA_STATUS_SUCCESS) { + Fatal(_("video/vaapi: Can't inititialize VA-API on '%s'"), + display_name); + } + s = vaQueryVendorString(VaDisplay); + Info(_("video/vaapi: libva %d.%d (%s) initialized\n"), major, minor, s); + + // + // Setup fixes for driver bugs. + // + if (strstr(s, "VDPAU")) { + Info(_("video/vaapi: use vdpau bug workaround\n")); + setenv("VDPAU_VIDEO_PUTSURFACE_FAST", "0", 0); + VaapiBuggyVdpau = 1; + } + // + // check if driver makes a copy of the VA surface for display. + // + attr.type = VADisplayAttribDirectSurface; + attr.flags = VA_DISPLAY_ATTRIB_GETTABLE; + if (vaGetDisplayAttributes(VaDisplay, &attr, 1) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: Can't get direct-surface attribute\n")); + attr.value = 1; + } + Info(_("video/vaapi: VA surface is %s\n"), + attr.value ? _("direct mapped") : _("copied")); + // FIXME: handle the cases: new liba: Don't use it. + +#if 0 + // + // check the chroma format + // + attr.type = VAConfigAttribRTFormat attr.flags = VA_DISPLAY_ATTRIB_GETTABLE; +#endif +} + +/// +/// VA-API cleanup +/// +static void VideoVaapiExit(void) +{ + int i; + + // FIXME: more VA-API cleanups... + + if (VaOsdImage.image_id != VA_INVALID_ID) { + if (vaDestroyImage(VaDisplay, + VaOsdImage.image_id) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy image!\n")); + } + VaOsdImage.image_id = VA_INVALID_ID; + } + + if (VaOsdSubpicture != VA_INVALID_ID) { + if (vaDestroySubpicture(VaDisplay, VaOsdSubpicture) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't destroy subpicture\n")); + } + VaOsdSubpicture = VA_INVALID_ID; + } + + for (i = 0; i < VaapiDecoderN; ++i) { + if (VaapiDecoders[i]) { + VaapiDelDecoder(VaapiDecoders[i]); + VaapiDecoders[i] = NULL; + } + } + VaapiDecoderN = 0; + + if (!VaDisplay) { + vaTerminate(VaDisplay); + VaDisplay = NULL; + } +} + +/** +** Update output for new size or aspect ratio. +** +** @param decoder VA-API decoder +*/ +static void VaapiUpdateOutput(VaapiDecoder * decoder) +{ + AVRational input_aspect_ratio; + AVRational display_aspect_ratio; + + input_aspect_ratio = decoder->InputAspect; + if (!input_aspect_ratio.num || !input_aspect_ratio.den) { + input_aspect_ratio.num = 1; + input_aspect_ratio.den = 1; + Debug(3, "video: aspect defaults to %d:%d\n", input_aspect_ratio.num, + input_aspect_ratio.den); + } + + av_reduce(&display_aspect_ratio.num, &display_aspect_ratio.den, + decoder->InputWidth * input_aspect_ratio.num, + decoder->InputHeight * input_aspect_ratio.den, 1024 * 1024); + + Debug(3, "video: aspect %d : %d\n", display_aspect_ratio.num, + display_aspect_ratio.den); + + // FIXME: store different positions for the ratios + + decoder->OutputX = 0; + decoder->OutputY = 0; + decoder->OutputWidth = (VideoWindowHeight * display_aspect_ratio.num) + / display_aspect_ratio.den; + decoder->OutputHeight = (VideoWindowWidth * display_aspect_ratio.num) + / display_aspect_ratio.den; + if ((unsigned)decoder->OutputWidth > VideoWindowWidth) { + decoder->OutputWidth = VideoWindowWidth; + decoder->OutputY = (VideoWindowHeight - decoder->OutputHeight) / 2; + } else { + decoder->OutputHeight = VideoWindowHeight; + decoder->OutputX = (VideoWindowWidth - decoder->OutputWidth) / 2; + } +} + +/** +** Find VA-API profile. +** +** Check if the requested profile is supported by VA-API. +** +** @param profiles a table of all supported profiles +** @param n number of supported profiles +** @param profile requested profile +** +** @returns the profile if supported, -1 if unsupported. +*/ +static VAProfile VaapiFindProfile(const VAProfile * profiles, unsigned n, + VAProfile profile) +{ + unsigned u; + + for (u = 0; u < n; ++u) { + if (profiles[u] == profile) { + return profile; + } + } + return -1; +} + +/** +** Find VA-API entry point. +** +** Check if the requested entry point is supported by VA-API. +** +** @param entrypoints a table of all supported entrypoints +** @param n number of supported entrypoints +** @param entrypoint requested entrypoint +** +** @returns the entry point if supported, -1 if unsupported. +*/ +static VAEntrypoint VaapiFindEntrypoint(const VAEntrypoint * entrypoints, + unsigned n, VAEntrypoint entrypoint) +{ + unsigned u; + + for (u = 0; u < n; ++u) { + if (entrypoints[u] == entrypoint) { + return entrypoint; + } + } + return -1; +} + +/** +** Callback to negotiate the PixelFormat. +** +** @param fmt is the list of formats which are supported by the codec, +** it is terminated by -1 as 0 is a valid format, the +** formats are ordered by quality. +*/ +static enum PixelFormat Vaapi_get_format(VaapiDecoder * decoder, + AVCodecContext * video_ctx, const enum PixelFormat *fmt) +{ + const enum PixelFormat *fmt_idx; + VAProfile profiles[vaMaxNumProfiles(VaDisplay)]; + int profile_n; + VAEntrypoint entrypoints[vaMaxNumEntrypoints(VaDisplay)]; + int entrypoint_n; + int p; + int e; + VAConfigAttrib attrib; + + Debug(3, "video: new stream format %d\n", GetMsTicks() - VideoSwitch); + // create initial black surface and display + VaapiBlackSurface(decoder); + VaapiCleanup(decoder); + + if (getenv("NO_HW")) { // FIXME: make config option + goto slow_path; + } + + p = -1; + e = -1; + + // prepare va-api profiles + if (vaQueryConfigProfiles(VaDisplay, profiles, &profile_n)) { + Error(_("codec: vaQueryConfigProfiles failed")); + goto slow_path; + } + Debug(3, "codec: %d profiles\n", profile_n); + + // check profile + switch (video_ctx->codec_id) { + case CODEC_ID_MPEG2VIDEO: + p = VaapiFindProfile(profiles, profile_n, VAProfileMPEG2Main); + break; + case CODEC_ID_MPEG4: + case CODEC_ID_H263: + p = VaapiFindProfile(profiles, profile_n, + VAProfileMPEG4AdvancedSimple); + break; + case CODEC_ID_H264: + // try more simple formats, fallback to better + if (video_ctx->profile == FF_PROFILE_H264_BASELINE) { + p = VaapiFindProfile(profiles, profile_n, + VAProfileH264Baseline); + if (p == -1) { + p = VaapiFindProfile(profiles, profile_n, + VAProfileH264Main); + } + } else if (video_ctx->profile == FF_PROFILE_H264_MAIN) { + p = VaapiFindProfile(profiles, profile_n, VAProfileH264Main); + } + if (p == -1) { + p = VaapiFindProfile(profiles, profile_n, VAProfileH264High); + } + break; + case CODEC_ID_WMV3: + p = VaapiFindProfile(profiles, profile_n, VAProfileVC1Main); + break; + case CODEC_ID_VC1: + p = VaapiFindProfile(profiles, profile_n, VAProfileVC1Advanced); + break; + default: + goto slow_path; + } + if (p == -1) { + Debug(3, "\tno profile found\n"); + goto slow_path; + } + Debug(3, "\tprofile %d\n", p); + + // prepare va-api entry points + if (vaQueryConfigEntrypoints(VaDisplay, p, entrypoints, &entrypoint_n)) { + Error(_("codec: vaQueryConfigEntrypoints failed")); + goto slow_path; + } + Debug(3, "codec: %d entrypoints\n", entrypoint_n); + // look through formats + for (fmt_idx = fmt; *fmt_idx != PIX_FMT_NONE; fmt_idx++) { + Debug(3, "\t%#010x %s\n", *fmt_idx, av_get_pix_fmt_name(*fmt_idx)); + // check supported pixel format with entry point + switch (*fmt_idx) { + case PIX_FMT_VAAPI_VLD: + e = VaapiFindEntrypoint(entrypoints, entrypoint_n, + VAEntrypointVLD); + break; + case PIX_FMT_VAAPI_MOCO: + case PIX_FMT_VAAPI_IDCT: + Debug(3, "codec: this VA-API pixel format is not supported\n"); + default: + continue; + } + if (e != -1) { + Debug(3, "\tentry point %d\n", e); + break; + } + } + if (e == -1) { + Warning(_("codec: unsupported: slow path\n")); + goto slow_path; + } + // + // prepare decoder + // + memset(&attrib, 0, sizeof(attrib)); + attrib.type = VAConfigAttribRTFormat; + if (vaGetConfigAttributes(decoder->VaDisplay, p, e, &attrib, 1)) { + Error(_("codec: can't get attributes")); + goto slow_path; + } + if (attrib.value & VA_RT_FORMAT_YUV420) { + Info(_("codec: YUV 420 supported\n")); + } + if (attrib.value & VA_RT_FORMAT_YUV422) { + Info(_("codec: YUV 422 supported\n")); + } + if (attrib.value & VA_RT_FORMAT_YUV444) { + Info(_("codec: YUV 444 supported\n")); + } + + if (!(attrib.value & VA_RT_FORMAT_YUV420)) { + Warning(_("codec: YUV 420 not supported\n")); + goto slow_path; + } + // create a configuration for the decode pipeline + if (vaCreateConfig(decoder->VaDisplay, p, e, &attrib, 1, + &decoder->VaapiContext->config_id)) { + Error(_("codec: can't create config")); + goto slow_path; + } + // FIXME: need only to create and destroy surfaces for size changes! + VaapiCreateSurfaces(decoder, video_ctx->width, video_ctx->height); + + // bind surfaces to context + if (vaCreateContext(decoder->VaDisplay, decoder->VaapiContext->config_id, + video_ctx->width, video_ctx->height, VA_PROGRESSIVE, + decoder->SurfacesFree, decoder->SurfaceFreeN, + &decoder->VaapiContext->context_id)) { + Error(_("codec: can't create context")); + goto slow_path; + } + + decoder->InputX = 0; + decoder->InputY = 0; + decoder->InputWidth = video_ctx->width; + decoder->InputHeight = video_ctx->height; + decoder->InputAspect = video_ctx->sample_aspect_ratio; + VaapiUpdateOutput(decoder); + +#ifdef USE_GLX + if (GlxEnabled) { + GlxSetupDecoder(decoder); + // FIXME: try two textures, but vdpau-backend supports only 1 surface + if (vaCreateSurfaceGLX(decoder->VaDisplay, GL_TEXTURE_2D, + decoder->GlTexture[0], &decoder->GlxSurface[0]) + != VA_STATUS_SUCCESS) { + Fatal(_("video: can't create glx surfaces")); + } + // FIXME: this isn't usable with vdpau-backend + /* + if (vaCreateSurfaceGLX(decoder->VaDisplay, GL_TEXTURE_2D, + decoder->GlTexture[1], &decoder->GlxSurface[1]) + != VA_STATUS_SUCCESS) { + Fatal(_("video: can't create glx surfaces")); + } + */ + } +#endif + + Debug(3, "\tpixel format %#010x\n", *fmt_idx); + return *fmt_idx; + + slow_path: + // no accelerated format found + video_ctx->hwaccel_context = NULL; + return avcodec_default_get_format(video_ctx, fmt); +} + +/** +** Draw surface of the VA-API decoder with x11. +** +** vaPutSurface with intel backend does sync on v-sync. +** +** @param decoder VA-API decoder +** @param surface VA-API surface id +** @param interlaced flag interlaced source +** @param top_field_first flag top_field_first for interlaced source +** @param field interlaced draw: 0 first field, 1 second field +*/ +static void VaapiPutSurfaceX11(VaapiDecoder * decoder, VASurfaceID surface, + int interlaced, int top_field_first, int field) +{ + unsigned type; + VAStatus status; + + // fixes: [drm:i915_hangcheck_elapsed] *ERROR* Hangcheck + // timer elapsed... GPU hung + usleep(1 * 1000); + + // deinterlace + if (interlaced && VideoDeinterlace != VideoDeinterlaceWeave) { + if (top_field_first) { + if (field) { + type = VA_BOTTOM_FIELD; + } else { + type = VA_TOP_FIELD; + } + } else { + if (field) { + type = VA_TOP_FIELD; + } else { + type = VA_BOTTOM_FIELD; + } + } + } else { + type = VA_FRAME_PICTURE; + } + + if ((status = vaPutSurface(decoder->VaDisplay, surface, decoder->Window, + // decoder src + decoder->InputX, decoder->InputY, decoder->InputWidth, + decoder->InputHeight, + // video dst + decoder->OutputX, decoder->OutputY, decoder->OutputWidth, + decoder->OutputHeight, NULL, 0, + type | decoder->SurfaceFlags)) != VA_STATUS_SUCCESS) { + // switching video kills VdpPresentationQueueBlockUntilSurfaceIdle + Error(_("video/vaapi: vaPutSurface failed %d\n"), status); + } + + if (0) { + // check if surface is really ready + // VDPAU backend, says always ready + VASurfaceStatus status; + + if (vaQuerySurfaceStatus(decoder->VaDisplay, surface, &status) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaQuerySurface failed\n")); + status = VASurfaceReady; + } + if (status != VASurfaceReady) { + Warning(_ + ("video/vaapi: surface %#x not ready: still displayed %d\n"), + surface, status); + return; + } + if (vaSyncSurface(decoder->VaDisplay, surface) != VA_STATUS_SUCCESS) { + Error(_("video: vaSyncSurface failed\n")); + } + + } + + if (0) { + int i; + + // look how the status changes the next 40ms + for (i = 0; i < 40; ++i) { + VASurfaceStatus status; + + if (vaQuerySurfaceStatus(VaDisplay, surface, + &status) != VA_STATUS_SUCCESS) { + Error(_("video: vaQuerySurface failed\n")); + } + Debug(3, "video/vaapi: %2d %d\n", i, status); + usleep(1 * 1000); + } + } +} + +#ifdef USE_GLX + +/** +** Render texture. +** +** @param texture 2d texture +*/ +static inline void VideoRenderTexture(GLuint texture, int x, int y, int width, + int height) +{ + glEnable(GL_TEXTURE_2D); + glBindTexture(GL_TEXTURE_2D, texture); + + glColor4f(1.0f, 1.0f, 1.0f, 1.0f); // no color + glBegin(GL_QUADS); { + glTexCoord2f(1.0f, 1.0f); + glVertex2i(x + width, y + height); + glTexCoord2f(0.0f, 1.0f); + glVertex2i(x, y + height); + glTexCoord2f(0.0f, 0.0f); + glVertex2i(x, y); + glTexCoord2f(1.0f, 0.0f); + glVertex2i(x + width, y); +#if 0 + glTexCoord2f(0.0f, 0.0f); + glVertex2i(x, y); + glTexCoord2f(0.0f, 1.0f); + glVertex2i(x, y + height); + glTexCoord2f(1.0f, 1.0f); + glVertex2i(x + width, y + height); + glTexCoord2f(1.0f, 0.0f); + glVertex2i(x + width, y); +#endif + } + glEnd(); + + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); +} + +/** +** Draw surface of the VA-API decoder with glx. +** +** @param decoder VA-API decoder +** @param surface VA-API surface id +** @param interlaced flag interlaced source +** @param top_field_first flag top_field_first for interlaced source +** @param field interlaced draw: 0 first field, 1 second field +*/ +static void VaapiPutSurfaceGLX(VaapiDecoder * decoder, VASurfaceID surface, + int interlaced, int top_field_first, int field) +{ + unsigned type; + uint32_t start; + uint32_t copy; + uint32_t end; + + // deinterlace + if (interlaced && VideoDeinterlace != VideoDeinterlaceWeave) { + if (top_field_first) { + if (field) { + type = VA_BOTTOM_FIELD; + } else { + type = VA_TOP_FIELD; + } + } else { + if (field) { + type = VA_TOP_FIELD; + } else { + type = VA_BOTTOM_FIELD; + } + } + } else { + type = VA_FRAME_PICTURE; + } + start = GetMsTicks(); + if (vaCopySurfaceGLX(decoder->VaDisplay, decoder->GlxSurface[0], surface, + type | decoder->SurfaceFlags) != VA_STATUS_SUCCESS) { + Error(_("video: vaCopySurfaceGLX failed\n")); + return; + } + copy = GetMsTicks(); + // hardware surfaces are always busy + VideoRenderTexture(decoder->GlTexture[0], decoder->OutputX, + decoder->OutputY, decoder->OutputWidth, decoder->OutputHeight); + end = GetMsTicks(); + //Debug(3, "video/vaapi/glx: %d copy %d render\n", copy - start, end - copy); +} + +#endif + +/** +** Find VA-API image format. +** +** @param decoder VA-API decoder +** @param pix_fmt ffmpeg pixel format +** @param[out] format image format +** +** FIXME: can fallback from I420 to YV12, if not supported +** FIXME: must check if put/get with this format is supported (see intel) +*/ +static int VaapiFindImageFormat(VaapiDecoder * decoder, + enum PixelFormat pix_fmt, VAImageFormat * format) +{ + VAImageFormat *imgfrmts; + int imgfrmt_n; + int i; + unsigned fourcc; + + switch (pix_fmt) { // convert ffmpeg to VA-API + // NV12, YV12, I420, BGRA + // intel: I420 is native format for MPEG-2 decoded surfaces + // intel: NV12 is native format for H.264 decoded surfaces + case PIX_FMT_YUV420P: + fourcc = VA_FOURCC_YV12; // YVU + fourcc = VA_FOURCC('I', '4', '2', '0'); // YUV + // FIXME: intel deinterlace ... only supported with nv12 + break; + case PIX_FMT_NV12: + fourcc = VA_FOURCC_NV12; + break; + default: + Fatal(_("video/vaapi: unsupported pixel format %d\n"), pix_fmt); + } + + imgfrmt_n = vaMaxNumImageFormats(decoder->VaDisplay); + imgfrmts = alloca(imgfrmt_n * sizeof(*imgfrmts)); + + if (vaQueryImageFormats(decoder->VaDisplay, imgfrmts, &imgfrmt_n) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaQueryImageFormats failed\n")); + return 0; + } + Debug(3, "video/vaapi: search format %c%c%c%c in %d image formats\n", + fourcc, fourcc >> 8, fourcc >> 16, fourcc >> 24, imgfrmt_n); + Debug(3, "video/vaapi: supported image formats:\n"); + for (i = 0; i < imgfrmt_n; ++i) { + Debug(3, "video/vaapi:\t%c%c%c%c\t%d\n", imgfrmts[i].fourcc, + imgfrmts[i].fourcc >> 8, imgfrmts[i].fourcc >> 16, + imgfrmts[i].fourcc >> 24, imgfrmts[i].depth); + } + // + // search image format + // + for (i = 0; i < imgfrmt_n; ++i) { + if (imgfrmts[i].fourcc == fourcc) { + *format = imgfrmts[i]; + Debug(3, "video/vaapi: use\t%c%c%c%c\t%d\n", imgfrmts[i].fourcc, + imgfrmts[i].fourcc >> 8, imgfrmts[i].fourcc >> 16, + imgfrmts[i].fourcc >> 24, imgfrmts[i].depth); + return 1; + } + } + + Fatal("video/vaapi: pixel format %d unsupported by VA-API\n", pix_fmt); + + return 0; +} + +/** +** Configure VA-API for new video format. +** +** @note called only for software decoder. +*/ +static void VaapiSetup(VaapiDecoder * decoder, AVCodecContext * video_ctx) +{ + int width; + int height; + VAImageFormat format[1]; + + // create initial black surface and display + VaapiBlackSurface(decoder); + // cleanup last context + VaapiCleanup(decoder); + + width = video_ctx->width; + height = video_ctx->height; + if (decoder->Image->image_id != VA_INVALID_ID) { + if (vaDestroyImage(VaDisplay, decoder->Image->image_id) + != VA_STATUS_SUCCESS) { + Error("video: can't destroy image!\n"); + } + } + VaapiFindImageFormat(decoder, video_ctx->pix_fmt, format); + + if (vaCreateImage(VaDisplay, format, width, height, + decoder->Image) != VA_STATUS_SUCCESS) { + Fatal("video: can't create image!\n"); + } + Debug(3, + "video/vaapi: created image %dx%d with id 0x%08x and buffer id 0x%08x\n", + width, height, decoder->Image->image_id, decoder->Image->buf); + + VaapiCreateSurfaces(decoder, width, height); + +#ifdef USE_GLX + if (GlxEnabled) { + // FIXME: destroy old context + + GlxSetupDecoder(decoder); + // FIXME: try two textures + if (vaCreateSurfaceGLX(decoder->VaDisplay, GL_TEXTURE_2D, + decoder->GlTexture[0], &decoder->GlxSurface[0]) + != VA_STATUS_SUCCESS) { + Fatal(_("video: can't create glx surfaces")); + } + /* + if (vaCreateSurfaceGLX(decoder->VaDisplay, GL_TEXTURE_2D, + decoder->GlTexture[1], &decoder->GlxSurface[1]) + != VA_STATUS_SUCCESS) { + Fatal(_("video: can't create glx surfaces")); + } + */ + } +#endif +} + +/// +/// Queue output surface. +/// +/// @param decoder VA-API decoder +/// @param surface output surface +/// @param softdec software decoder +/// +/// @note we can't mix software and hardware decoder surfaces +/// +static void VaapiQueueSurface(VaapiDecoder * decoder, VASurfaceID surface, + int softdec) +{ + VASurfaceID old; + + ++decoder->FrameCounter; + + if (1) { // can't wait for output queue empty + if (atomic_read(&decoder->SurfacesFilled) >= VIDEO_SURFACES_MAX) { + ++decoder->FramesDropped; + Warning(_("video: output buffer full, dropping frame (%d/%d)\n"), + decoder->FramesDropped, decoder->FrameCounter); + if (!(decoder->FrameCounter % 100)) { + VaapiPrintFrames(decoder); + } + if (softdec) { // software surfaces only + VaapiReleaseSurface(decoder, surface); + } + return; + } + } else { // wait for output queue empty + while (atomic_read(&decoder->SurfacesFilled) >= VIDEO_SURFACES_MAX) { + VideoDisplayHandler(); + } + } + + // + // Check and release, old surface + // + if ((old = decoder->SurfacesRb[decoder->SurfaceWrite]) + != VA_INVALID_ID) { + + if (vaSyncSurface(decoder->VaDisplay, old) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } +#if 0 + VASurfaceStatus status; + + if (vaQuerySurfaceStatus(decoder->VaDisplay, old, &status) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaQuerySurface failed\n")); + status = VASurfaceReady; + } + if (status != VASurfaceReady) { + Warning(_ + ("video/vaapi: surface %#x not ready: still displayed %d\n"), + old, status); + if (vaSyncSurface(decoder->VaDisplay, old) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + } +#endif + + // now we can release the surface + if (softdec) { // software surfaces only + VaapiReleaseSurface(decoder, old); + } + } +#if 0 + // + // associate the OSD with surface + // + if (vaAssociateSubpicture(VaDisplay, VaOsdSubpicture, &surface, 1, 0, 0, + VaOsdImage.width, VaOsdImage.height, 0, 0, decoder->InputWidth, + decoder->InputHeight, 0) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't associate subpicture\n")); + } +#endif + + decoder->SurfacesRb[decoder->SurfaceWrite] = surface; + decoder->SurfaceWrite = (decoder->SurfaceWrite + 1) + % VIDEO_SURFACES_MAX; + atomic_inc(&decoder->SurfacesFilled); + + Debug(4, "video/vaapi: yy video surface %#x ready\n", surface); +} + +#if 0 + + /// Return the absolute value of an integer. +#define ABS(i) ((i) >= 0 ? (i) : (-(i))) + +/// +/// ELA Edge-based Line Averaging +/// Low-Complexity Interpolation Method +/// +/// abcdefg abcdefg abcdefg abcdefg abcdefg +/// x x x x x +/// hijklmn hijklmn hijklmn hijklmn hijklmn +/// +static void FilterLine(const uint8_t * past, const uint8_t * cur, + const uint8_t * future, int width, int above, int below) +{ + int a, b, c, d, e, f, g, h, i, j, k, l, m, n; +} + +#endif + +/// +/// Create and display a black empty surface. +/// +/// @param decoder VA-API decoder +/// +static void VaapiBlackSurface(VaapiDecoder * decoder) +{ + VAStatus status; + + // wait until we have osd subpicture + if (VaOsdSubpicture == VA_INVALID_ID) { + Warning(_("video/vaapi: no osd subpicture yet\n")); + return; + } + + if (decoder->BlackSurface == VA_INVALID_ID) { + if (vaCreateSurfaces(decoder->VaDisplay, VideoWindowWidth, + VideoWindowHeight, VA_RT_FORMAT_YUV420, 1, + &decoder->BlackSurface) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't create a surface\n")); + return; + } + } + + if (vaAssociateSubpicture(decoder->VaDisplay, VaOsdSubpicture, + &decoder->BlackSurface, 1, 0, 0, VaOsdImage.width, + VaOsdImage.height, 0, 0, VideoWindowWidth, VideoWindowHeight, + 0) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't associate subpicture\n")); + } + + if (vaSyncSurface(decoder->VaDisplay, + decoder->BlackSurface) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + + Debug(4, "video/vaapi: yy black video surface %#x displayed\n", + decoder->BlackSurface); + if ((status = + vaPutSurface(decoder->VaDisplay, decoder->BlackSurface, + decoder->Window, + // decoder src + 0, 0, VideoWindowWidth, VideoWindowHeight, + // video dst + 0, 0, VideoWindowWidth, VideoWindowHeight, NULL, 0, + VA_FRAME_PICTURE)) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaPutSurface failed %d\n"), status); + } + clock_gettime(CLOCK_REALTIME, &decoder->FrameTime); + + if (vaSyncSurface(decoder->VaDisplay, + decoder->BlackSurface) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + + usleep(1 * 1000); +} + +/// +/// Vaapi bob deinterlace. +/// +static void VaapiBob(VaapiDecoder * decoder, VAImage * src, VAImage * dst1, + VAImage * dst2) +{ + void *src_base; + void *dst1_base; + void *dst2_base; + unsigned y; + unsigned p; + + if (vaMapBuffer(decoder->VaDisplay, src->buf, + &src_base) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't map the image!\n"); + } + if (vaMapBuffer(decoder->VaDisplay, dst1->buf, + &dst1_base) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't map the image!\n"); + } + if (vaMapBuffer(decoder->VaDisplay, dst2->buf, + &dst2_base) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't map the image!\n"); + } + + if (1) { + memset(dst1_base, 0x00, dst1->data_size); + memset(dst2_base, 0x00, dst2->data_size); + } + for (p = 0; p < src->num_planes; ++p) { + for (y = 0; y < (unsigned)(src->height >> (p != 0)); y += 2) { + memcpy(dst1_base + src->offsets[p] + y * src->pitches[p], + src_base + src->offsets[p] + y * src->pitches[p], + src->pitches[p]); + memcpy(dst1_base + src->offsets[p] + (y + 1) * src->pitches[p], + src_base + src->offsets[p] + y * src->pitches[p], + src->pitches[p]); + + memcpy(dst2_base + src->offsets[p] + y * src->pitches[p], + src_base + src->offsets[p] + (y + 1) * src->pitches[p], + src->pitches[p]); + memcpy(dst2_base + src->offsets[p] + (y + 1) * src->pitches[p], + src_base + src->offsets[p] + (y + 1) * src->pitches[p], + src->pitches[p]); + } + } + + if (vaUnmapBuffer(decoder->VaDisplay, dst2->buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap image buffer\n")); + } + if (vaUnmapBuffer(decoder->VaDisplay, dst1->buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap image buffer\n")); + } + if (vaUnmapBuffer(decoder->VaDisplay, src->buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap image buffer\n")); + } +} + +/// +/// Vaapi software deinterlace. +/// +static void VaapiCpuDeinterlace(VaapiDecoder * decoder, VASurfaceID surface) +{ +#if 0 + VAImage image[1]; + VAStatus status; + VAImageFormat format[1]; + void *image_base; + int image_derived; + + // release old frame + // get new frame + // deinterlace + + image_derived = 1; + if ((status = + vaDeriveImage(decoder->VaDisplay, surface, + image)) != VA_STATUS_SUCCESS) { + image_derived = 0; + Warning(_("video/vaapi: vaDeriveImage failed %d\n"), status); + // NV12, YV12, I420, BGRA + VaapiFindImageFormat(decoder, PIX_FMT_YUV420P, format); + if (vaCreateImage(decoder->VaDisplay, format, decoder->InputWidth, + decoder->InputHeight, image) != VA_STATUS_SUCCESS) { + Fatal(_("video/vaapi: can't create image!\n")); + } + if (vaGetImage(decoder->VaDisplay, surface, 0, 0, decoder->InputWidth, + decoder->InputHeight, image->image_id) != VA_STATUS_SUCCESS) { + Fatal(_("video/vaapi: can't get image!\n")); + } + } + Debug(3, "video/vaapi: %c%c%c%c %dx%d*%d\n", image->format.fourcc, + image->format.fourcc >> 8, image->format.fourcc >> 16, + image->format.fourcc >> 24, image->width, image->height, + image->num_planes); + + if (vaMapBuffer(decoder->VaDisplay, image->buf, + &image_base) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't map the image!\n"); + } + + memset(image_base, 0xff, image->width * image->height); + + if (vaUnmapBuffer(decoder->VaDisplay, image->buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap image buffer\n")); + } + + if (!image_derived) { + if ((status = + vaPutImage(decoder->VaDisplay, surface, image->image_id, 0, 0, + image->width, image->height, 0, 0, image->width, + image->height)) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't put image %d!\n", status); + } + } + + vaDestroyImage(decoder->VaDisplay, image->image_id); +#endif + VAImage *img1; + VAImage *img2; + VAImage *img3; + VASurfaceID out1; + VASurfaceID out2; + + // + // Create deinterlace images. + // + if (decoder->DeintImages[0].image_id == VA_INVALID_ID) { + VAImageFormat format[1]; + int i; + + // NV12, YV12, I420, BGRA + // VaapiFindImageFormat(decoder, PIX_FMT_YUV420P, format); + + // Intel needs NV12 + VaapiFindImageFormat(decoder, PIX_FMT_NV12, format); + for (i = 0; i < 3; ++i) { + if (vaCreateImage(decoder->VaDisplay, format, decoder->InputWidth, + decoder->InputHeight, + decoder->DeintImages + i) != VA_STATUS_SUCCESS) { + Fatal(_("video/vaapi: can't create image!\n")); + } + } + img1 = decoder->DeintImages; + Debug(3, "video/vaapi: %c%c%c%c %dx%d*%d\n", img1->format.fourcc, + img1->format.fourcc >> 8, img1->format.fourcc >> 16, + img1->format.fourcc >> 24, img1->width, img1->height, + img1->num_planes); + } + + if (vaSyncSurface(decoder->VaDisplay, surface) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + + img1 = decoder->DeintImages; + img2 = decoder->DeintImages + 1; + img3 = decoder->DeintImages + 2; + + if (vaGetImage(decoder->VaDisplay, surface, 0, 0, decoder->InputWidth, + decoder->InputHeight, img1->image_id) != VA_STATUS_SUCCESS) { + Fatal(_("video/vaapi: can't get img1!\n")); + } + + VaapiBob(decoder, img1, img2, img3); + + // get a free surface and upload the image + out1 = VaapiGetSurface(decoder); + if (vaPutImage(VaDisplay, out1, img2->image_id, 0, 0, img2->width, + img2->height, 0, 0, img2->width, + img2->height) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't put image!\n"); + } + VaapiQueueSurface(decoder, out1, 1); + if (vaSyncSurface(decoder->VaDisplay, out1) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + // get a free surface and upload the image + out2 = VaapiGetSurface(decoder); + if (vaPutImage(VaDisplay, out2, img3->image_id, 0, 0, img3->width, + img3->height, 0, 0, img3->width, + img3->height) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't put image!\n"); + } + VaapiQueueSurface(decoder, out2, 1); + if (vaSyncSurface(decoder->VaDisplay, out2) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + // FIXME: must release software input surface +} + +/// +/// Render a ffmpeg frame +/// +/// @param decoder VA-API decoder +/// @param video_ctx ffmpeg video codec context +/// @param frame frame to display +/// +static void VaapiRenderFrame(VaapiDecoder * decoder, + AVCodecContext * video_ctx, AVFrame * frame) +{ + VASurfaceID surface; + + if (video_ctx->height != decoder->InputHeight + || video_ctx->width != decoder->InputWidth) { + Debug(3, "video/vaapi: stream <-> surface size mismatch\n"); + } + // + // Hardware render + // + if (video_ctx->hwaccel_context) { + int interlaced; + + surface = (unsigned)(size_t) frame->data[3]; + Debug(4, "video/vaapi: hw render hw surface %#x\n", surface); + + // FIXME: some tv-stations toggle interlace on/off + // frame->interlaced_frame isn't always correct set + interlaced = frame->interlaced_frame; + if (video_ctx->height == 720) { + if (interlaced && !decoder->WrongInterlacedWarned) { + Debug(3, "video/vaapi: wrong interlace flag fixed\n"); + decoder->WrongInterlacedWarned = 1; + } + interlaced = 0; + } else { + if (!interlaced && !decoder->WrongInterlacedWarned) { + Debug(3, "video/vaapi: wrong interlace flag fixed\n"); + decoder->WrongInterlacedWarned = 1; + } + interlaced = 1; + } + + // update aspect ratio changes + if (av_cmp_q(decoder->InputAspect, frame->sample_aspect_ratio)) { + Debug(3, "video/vaapi: aspect ratio changed\n"); + + //decoder->InputWidth = video_ctx->width; + //decoder->InputHeight = video_ctx->height; + decoder->InputAspect = frame->sample_aspect_ratio; + VaapiUpdateOutput(decoder); + } + + if (VideoDeinterlace == VideoDeinterlaceSoftware && interlaced) { + // FIXME: software deinterlace avpicture_deinterlace + VaapiCpuDeinterlace(decoder, surface); + } else { + // FIXME: should be done by init + if (decoder->Interlaced != interlaced + || decoder->TopFieldFirst != frame->top_field_first) { + + Debug(3, "video/vaapi: interlaced %d top-field-first %d\n", + interlaced, frame->top_field_first); + + decoder->Interlaced = interlaced; + decoder->TopFieldFirst = frame->top_field_first; + + } + VaapiQueueSurface(decoder, surface, 0); + } + + // + // VAImage render + // + } else { + void *va_image_data; + int i; + AVPicture picture[1]; + int width; + int height; + + Debug(4, "video/vaapi: hw render sw surface\n"); + + width = video_ctx->width; + height = video_ctx->height; + // + // Check image, format, size + // + if (decoder->Image->image_id == VA_INVALID_ID + || decoder->PixFmt != video_ctx->pix_fmt + || width != decoder->InputWidth + || height != decoder->InputHeight) { + + decoder->PixFmt = video_ctx->pix_fmt; + decoder->InputX = 0; + decoder->InputY = 0; + decoder->InputWidth = width; + decoder->InputHeight = height; + + VaapiSetup(decoder, video_ctx); + + // + // detect interlaced input + // + Debug(3, "video/vaapi: interlaced %d top-field-first %d\n", + frame->interlaced_frame, frame->top_field_first); + + decoder->Interlaced = frame->interlaced_frame; + decoder->TopFieldFirst = frame->top_field_first; + // FIXME: I hope this didn't change in the middle of the stream + } + // FIXME: Need to insert software deinterlace here + + // + // Copy data from frame to image + // + if (vaMapBuffer(VaDisplay, decoder->Image->buf, &va_image_data) + != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't map the image!\n"); + } + for (i = 0; (unsigned)i < decoder->Image->num_planes; ++i) { + picture->data[i] = va_image_data + decoder->Image->offsets[i]; + picture->linesize[i] = decoder->Image->pitches[i]; + } + + av_picture_copy(picture, (AVPicture *) frame, video_ctx->pix_fmt, + width, height); + + if (vaUnmapBuffer(VaDisplay, decoder->Image->buf) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't unmap the image!\n"); + } + // get a free surface and upload the image + surface = VaapiGetSurface(decoder); + + // FIXME: intel didn't support put image. + if ((i = vaPutImage(VaDisplay, surface, decoder->Image->image_id, 0, 0, + width, height, 0, 0, width, height) + ) != VA_STATUS_SUCCESS) { + Fatal("video/vaapi: can't put image %d!\n", i); + } + + VaapiQueueSurface(decoder, surface, 1); + } + + if (decoder->Interlaced) { + ++decoder->FrameCounter; + } +} + +/** +** Video render frame. +** +** FIXME: no locks for multi-thread +** FIXME: frame delay for 50hz hardcoded +** +*/ +void VaapiDisplayFrame(void) +{ + uint32_t start; + uint32_t sync; + uint32_t put1; + uint32_t put2; + int i; + VaapiDecoder *decoder; + VASurfaceID surface; + + // look if any stream have a new surface available + for (i = 0; i < VaapiDecoderN; ++i) { + int filled; + + decoder = VaapiDecoders[i]; + filled = atomic_read(&decoder->SurfacesFilled); + if (filled) { + // show any frame as fast as possible + // we keep always the last frame in the ring buffer + if (filled > 1) { + decoder->SurfaceRead = (decoder->SurfaceRead + 1) + % VIDEO_SURFACES_MAX; + atomic_dec(&decoder->SurfacesFilled); + } + + start = GetMsTicks(); + surface = decoder->SurfacesRb[decoder->SurfaceRead]; + Debug(4, "video/vaapi: yy video surface %#x displayed\n", surface); + + if (vaSyncSurface(decoder->VaDisplay, surface) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: vaSyncSurface failed\n")); + } + + sync = GetMsTicks(); + VaapiPutSurfaceX11(decoder, surface, decoder->Interlaced, + decoder->TopFieldFirst, 0); + put1 = GetMsTicks(); + put2 = put1; + // deinterlace and full frame rate + if (decoder->Interlaced) { + VaapiPutSurfaceX11(decoder, surface, decoder->Interlaced, + decoder->TopFieldFirst, 1); + // FIXME: buggy libva-driver-vdpau. + if (VaapiBuggyVdpau + && VideoDeinterlace != VideoDeinterlaceWeave) { + VaapiPutSurfaceX11(decoder, surface, decoder->Interlaced, + decoder->TopFieldFirst, 0); + VaapiPutSurfaceX11(decoder, surface, decoder->Interlaced, + decoder->TopFieldFirst, 1); + } + put2 = GetMsTicks(); + } + xcb_flush(Connection); + Debug(4, "video/vaapi: sync %2u put1 %2u put2 %2u\n", sync - start, + put1 - sync, put2 - put1); + clock_gettime(CLOCK_REALTIME, &decoder->FrameTime); + } else { + Debug(3, "video/vaapi: no video surface ready\n"); + } + } +} + +/** +** Clear subpicture image. +** +** @note it is possible, that we need a lock here +*/ +static void VaapiOsdClear(void) +{ + void *image_buffer; + + // osd image available? + if (VaOsdImage.image_id == VA_INVALID_ID) { + return; + } + + Debug(3, "video/vaapi: clear image\n"); + + // map osd surface/image into memory. + if (vaMapBuffer(VaDisplay, VaOsdImage.buf, &image_buffer) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't map osd image buffer\n")); + return; + } + // 100% transparent + memset(image_buffer, 0x00, VaOsdImage.data_size); + + if (vaUnmapBuffer(VaDisplay, VaOsdImage.buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap osd image buffer\n")); + } +} + +/** +** Upload ARGB to subpicture image. +** +** @note it is possible, that we need a lock here +*/ +static void VaapiUploadImage(int x, int y, int width, int height, + const uint8_t * argb) +{ + void *image_buffer; + int o; + + // osd image available? + if (VaOsdImage.image_id == VA_INVALID_ID) { + return; + } + + Debug(3, "video/vaapi: upload image\n"); + + // map osd surface/image into memory. + if (vaMapBuffer(VaDisplay, VaOsdImage.buf, &image_buffer) + != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't map osd image buffer\n")); + return; + } + // 100% transparent + //memset(image_buffer, 0x00, VaOsdImage.data_size); + + // FIXME: convert image from ARGB to subpicture format, if not argb + + // copy argb to image + for (o = 0; o < height; ++o) { + memcpy(image_buffer + (x + (y + o) * VaOsdImage.width) * 4, + argb + o * width * 4, width * 4); + } + + if (vaUnmapBuffer(VaDisplay, VaOsdImage.buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap osd image buffer\n")); + } +} + +/** +** VA-API initialize OSD. +** +** Subpicture is unusable, its scaled with the video image. +*/ +static void VaapiOsdInit(int width, int height) +{ + VAImageFormat *formats; + unsigned *flags; + unsigned format_n; + unsigned u; + unsigned v; + static uint32_t wanted_formats[] = + { VA_FOURCC('B', 'G', 'R', 'A'), VA_FOURCC_RGBA }; + + if (VaOsdImage.image_id != VA_INVALID_ID) { + Debug(3, "video/vaapi: osd already setup\n"); + return; + } + + if (!VaDisplay) { + Debug(3, "video/vaapi: va-api not setup\n"); + return; + } + // + // look through subpicture formats + // + format_n = vaMaxNumSubpictureFormats(VaDisplay); + formats = alloca(format_n * sizeof(*formats)); + flags = alloca(format_n * sizeof(*formats)); + if (vaQuerySubpictureFormats(VaDisplay, formats, flags, + &format_n) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't get subpicture formats")); + return; + } +#ifdef DEBUG + Debug(3, "video/vaapi: supported subpicture formats:\n"); + for (u = 0; u < format_n; ++u) { + Debug(3, "video/vaapi:\t%c%c%c%c flags %#x %s\n", formats[u].fourcc, + formats[u].fourcc >> 8, formats[u].fourcc >> 16, + formats[u].fourcc >> 24, flags[u], + flags[u] & VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD ? + "screen coord" : ""); + } +#endif + for (v = 0; v < sizeof(wanted_formats) / sizeof(*wanted_formats); ++v) { + for (u = 0; u < format_n; ++u) { + if (formats[u].fourcc == wanted_formats[v]) { + goto found; + } + } + } + Error(_("video/vaapi: can't find a supported subpicture format")); + return; + + found: + Debug(3, "video/vaapi: use %c%c%c%c subpicture format with flags %#x\n", + formats[u].fourcc, formats[u].fourcc >> 8, formats[u].fourcc >> 16, + formats[u].fourcc >> 24, flags[u]); + + VaapiUnscaledOsd = 0; + if (flags[u] & VA_SUBPICTURE_DESTINATION_IS_SCREEN_COORD) { + Info(_("video/vaapi: vaapi supports unscaled osd\n")); + VaapiUnscaledOsd = 1; + } + // FIXME: + VaapiUnscaledOsd = 0; + Info(_("video/vaapi: unscaled osd disabled\n")); + + if (vaCreateImage(VaDisplay, &formats[u], width, height, + &VaOsdImage) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't create osd image\n")); + return; + } + if (vaCreateSubpicture(VaDisplay, VaOsdImage.image_id, + &VaOsdSubpicture) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't create subpicture\n")); + return; + } + // FIXME: must store format, to convert ARGB to it. + + VaapiOsdClear(); +} + +#endif + +//---------------------------------------------------------------------------- +// OSD +//---------------------------------------------------------------------------- + +//static int OsdShow; ///< flag show osd +static int OsdWidth; ///< osd width +static int OsdHeight; ///< osd height + +/** +** Clear the OSD. +** +** @todo I use glTexImage2D to clear the texture, are there faster and +** better ways to clear a texture? +*/ +void VideoOsdClear(void) +{ + VideoThreadLock(); +#ifdef USE_GLX + if (GlxEnabled) { + void *texbuf; + + texbuf = calloc(OsdWidth * OsdHeight, 4); + glEnable(GL_TEXTURE_2D); // 2d texture + glBindTexture(GL_TEXTURE_2D, OsdGlTextures[OsdIndex]); + // upload no image data, clears texture (on some drivers only) + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, OsdWidth, OsdHeight, 0, + GL_BGRA, GL_UNSIGNED_BYTE, texbuf); + glBindTexture(GL_TEXTURE_2D, 0); + glDisable(GL_TEXTURE_2D); + GlxCheck(); + free(texbuf); + } +#endif +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiOsdClear(); + VideoThreadUnlock(); + return; + } +#endif + VideoThreadUnlock(); +} + +/** +** Draw an OSD ARGB image. +*/ +void VideoOsdDrawARGB(int x, int y, int height, int width, + const uint8_t * argb) +{ + VideoThreadLock(); +#ifdef USE_GLX + if (GlxEnabled) { + Debug(3, "video: %p <-> %p\n", glXGetCurrentContext(), GlxContext); + GlxUploadTexture(x, y, height, width, argb); + VideoThreadUnlock(); + return; + } +#endif +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiUploadImage(x, y, height, width, argb); + VideoThreadUnlock(); + return; + } +#endif + (void)x; + (void)y; + (void)height; + (void)width; + (void)argb; + VideoThreadUnlock(); +} + +/** +** Setup osd. +** +** FIXME: looking for BGRA, but this fourcc isn't supported by the +** drawing functions yet. +*/ +void VideoOsdInit(void) +{ + OsdWidth = 1920 / 1; + OsdHeight = 1080 / 1; // worst-case + + //OsdWidth = 768; + //OsdHeight = VideoWindowHeight; // FIXME: must be configured + +#ifdef USE_GLX + // FIXME: make an extra function for this + if (GlxEnabled) { + int i; + + Debug(3, "video/glx: %p <-> %p\n", glXGetCurrentContext(), GlxContext); + + // + // create a RGBA texture. + // + glEnable(GL_TEXTURE_2D); // create 2d texture(s) + + glGenTextures(2, OsdGlTextures); + for (i = 0; i < 2; ++i) { + glBindTexture(GL_TEXTURE_2D, OsdGlTextures[i]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, + GL_CLAMP_TO_EDGE); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, + GL_CLAMP_TO_EDGE); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, OsdWidth, OsdHeight, 0, + GL_BGRA, GL_UNSIGNED_BYTE, NULL); + } + glBindTexture(GL_TEXTURE_2D, 0); + + glDisable(GL_TEXTURE_2D); + return; + } +#endif +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiOsdInit(OsdWidth, OsdHeight); + return; + } +#endif +} + +#if 0 + +//---------------------------------------------------------------------------- +// Overlay +//---------------------------------------------------------------------------- + +/** +** Render osd surface. +*/ +void VideoRenderOverlay(void) +{ +#ifdef USE_GLX + if (GlxEnabled) { + GlxRender(OsdWidth, OsdHeight); + } else +#endif + { + } +} + +/** +** Display overlay surface. +*/ +void VideoDisplayOverlay(void) +{ +#ifdef USE_GLX + if (GlxEnabled) { + int osd_x1; + int osd_y1; + + osd_x1 = 0; + osd_y1 = 0; +#ifdef noDEBUG + osd_x1 = 100; + osd_y1 = 100; +#endif + GlxRenderTexture(OsdGlTextures[OsdIndex], osd_x1, osd_y1, + VideoWindowWidth, VideoWindowHeight); + return; + } +#endif +#ifdef USE_VAAPI + { + void *image_buffer; + static int counter; + + // upload needs long time + if (counter == 5) { + //return; + } + // osd image available? + if (VaOsdImage.image_id == VA_INVALID_ID) { + return; + } + // FIXME: this version hangups + //return; + + // map osd surface/image into memory. + if (vaMapBuffer(VaDisplay, VaOsdImage.buf, + &image_buffer) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't map osd image buffer\n")); + return; + } + // 100% transparent + memset(image_buffer, 0x80 | counter++, VaOsdImage.data_size); + + // convert internal osd to VA-API image + //GfxConvert(image_buffer, VaOsdImage.offsets[0], VaOsdImage.pitches[0]); + + if (vaUnmapBuffer(VaDisplay, VaOsdImage.buf) != VA_STATUS_SUCCESS) { + Error(_("video/vaapi: can't unmap osd image buffer\n")); + } + } +#endif +} + +#endif + +//---------------------------------------------------------------------------- +// Frame +//---------------------------------------------------------------------------- + +/** +** Display a single frame. +*/ +static void VideoDisplayFrame(void) +{ +#ifdef USE_GLX + if (GlxEnabled) { + VideoDisplayOverlay(); + +#ifdef USE_DOUBLEBUFFER + glXSwapBuffers(XlibDisplay, VideoWindow); +#else + glFinish(); // wait for all execution finished +#endif + GlxCheck(); + + glClear(GL_COLOR_BUFFER_BIT); + } +#endif +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiDisplayFrame(); + return; + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + return; + } +#endif +} + +//---------------------------------------------------------------------------- +// Events +//---------------------------------------------------------------------------- + +/// C callback feed key press +extern void FeedKeyPress(const char *, const char *, int, int); + +/** +** Handle X11 events. +** +** @todo Signal WmDeleteMessage to application. +*/ +static void VideoEvent(void) +{ + XEvent event; + KeySym keysym; + + //char buf[32]; + + XNextEvent(XlibDisplay, &event); + switch (event.type) { + case ClientMessage: + Debug(3, "video/event: ClientMessage\n"); + if (event.xclient.data.l[0] == (long)WmDeleteWindowAtom) { + // FIXME: wrong, kills recordings ... + Error(_("video: FIXME: wm-delete-message\n")); + } + break; + + case MapNotify: + Debug(3, "video/event: MapNotify\n"); + break; + case Expose: + Debug(3, "video/event: Expose\n"); + break; + case ReparentNotify: + Debug(3, "video/event: ReparentNotify\n"); + break; + case ConfigureNotify: + Debug(3, "video/event: ConfigureNotify\n"); + break; + case KeyPress: + keysym = XLookupKeysym(&event.xkey, 0); + if (keysym == NoSymbol) { + Warning(_("video: No symbol for %d\n"), event.xkey.keycode); + } + FeedKeyPress("XKeySym", XKeysymToString(keysym), 0, 0); + /* + if (XLookupString(&event.xkey, buf, sizeof(buf), &keysym, NULL)) { + FeedKeyPress("XKeySym", buf, 0, 0); + } else { + FeedKeyPress("XKeySym", XKeysymToString(keysym), 0, 0); + } + */ + case KeyRelease: + break; + default: +#if 0 + if (XShmGetEventBase(XlibDisplay) + ShmCompletion == event.type) { + // printf("ShmCompletion\n"); + } +#endif + Debug(3, "Unsupported event type %d\n", event.type); + break; + } +} + +/** +** Poll all x11 events. +*/ +void VideoPollEvent(void) +{ + while (XPending(XlibDisplay)) { + VideoEvent(); + } +} + +//---------------------------------------------------------------------------- +// Thread +//---------------------------------------------------------------------------- + +#ifdef USE_VIDEO_THREAD + +static pthread_t VideoThread; ///< video decode thread +static pthread_cond_t VideoWakeupCond; ///< wakeup condition variable +static pthread_mutex_t VideoMutex; ///< video condition mutex +static pthread_mutex_t VideoLockMutex; ///< video lock mutex + +#ifdef USE_GLX +static GLXContext GlxThreadContext; ///< our gl context for the thread +#endif + +/** +** Lock video thread. +*/ +static void VideoThreadLock(void) +{ + if (pthread_mutex_lock(&VideoLockMutex)) { + Error(_("video: can't lock thread\n")); + } +} + +/** +** Unlock video thread. +*/ +static void VideoThreadUnlock(void) +{ + if (pthread_mutex_unlock(&VideoLockMutex)) { + Error(_("video: can't unlock thread\n")); + } +} + +/** +** Video render thread. +*/ +static void *VideoDisplayHandlerThread(void *dummy) +{ + Debug(3, "video: display thread started\n"); + +#ifdef USE_GLX + if (GlxEnabled) { + Debug(3, "video: %p <-> %p\n", glXGetCurrentContext(), + GlxThreadContext); + GlxThreadContext = + glXCreateContext(XlibDisplay, GlxVisualInfo, GlxContext, GL_TRUE); + if (!GlxThreadContext) { + Error(_("video/glx: can't create glx context\n")); + return NULL; + } + // set glx context + if (!glXMakeCurrent(XlibDisplay, VideoWindow, GlxThreadContext)) { + GlxCheck(); + Error(_("video/glx: can't make glx context current\n")); + return NULL; + } + } +#endif + + for (;;) { + int err; + int filled; + struct timespec nowtime; + struct timespec abstime; + VaapiDecoder *decoder; + uint64_t delay; + + decoder = VaapiDecoders[0]; + + VideoPollEvent(); + + // initial delay + delay = AudioGetDelay(); + if (delay < 100 * 90) { // no audio delay known + delay = 760 * 1000 * 1000; + } else { + delay = (delay * 1000 * 1000) / 90 + 60 * 1000 * 1000; + } + clock_gettime(CLOCK_REALTIME, &nowtime); + if (!atomic_read(&decoder->SurfacesFilled) + || (uint64_t) ((nowtime.tv_sec - decoder->StartTime.tv_sec) + * 1000 * 1000 * 1000 + (nowtime.tv_nsec - + decoder->StartTime.tv_nsec)) > delay) { + + if ((nowtime.tv_sec - decoder->StartTime.tv_sec) + * 1000 * 1000 * 1000 + (nowtime.tv_nsec - + decoder->StartTime.tv_nsec) + < 2000 * 1000 * 1000) { + Debug(3, "video: audio delay %lu ms\n", delay / (1000 * 1000)); + } + // FIXME: hot polling + pthread_mutex_lock(&VideoLockMutex); + // fetch or reopen + err = VideoDecode(); + pthread_mutex_unlock(&VideoLockMutex); + if (err) { + // FIXME: sleep on wakeup + usleep(5 * 1000); // nothing buffered + } + } else { + Debug(3, "video/vaapi: waiting %9lu ms\n", + ((nowtime.tv_sec - decoder->StartTime.tv_sec) + * 1000 * 1000 * 1000 + (nowtime.tv_nsec - + decoder->StartTime.tv_nsec)) / (1000 * 1000)); + + abstime = nowtime; + abstime.tv_nsec += 18 * 1000 * 1000; + if (abstime.tv_nsec >= 1000 * 1000 * 1000) { + // avoid overflow + abstime.tv_sec++; + abstime.tv_nsec -= 1000 * 1000 * 1000; + } + + pthread_mutex_lock(&VideoLockMutex); + // give osd some time slot + while (pthread_cond_timedwait(&VideoWakeupCond, &VideoLockMutex, + &abstime) != ETIMEDOUT) { + // SIGUSR1 + Debug(3, "video/vaapi: pthread_cond_timedwait error\n"); + } + pthread_mutex_unlock(&VideoLockMutex); + } + + clock_gettime(CLOCK_REALTIME, &nowtime); + // time for one frame over, buggy for vaapi-vdpau + if ((nowtime.tv_sec - decoder->FrameTime.tv_sec) * 1000 * 1000 * 1000 + + (nowtime.tv_nsec - decoder->FrameTime.tv_nsec) < + (decoder->Interlaced ? 17 : 17) * 1000 * 1000) { + continue; + } + + filled = atomic_read(&decoder->SurfacesFilled); + if (!filled) { + pthread_mutex_lock(&VideoLockMutex); + VaapiBlackSurface(decoder); + pthread_mutex_unlock(&VideoLockMutex); + } else if (filled == 1) { + decoder->FramesDuped++; + ++decoder->FrameCounter; + if (!(decoder->FrameCounter % 333)) { + Warning(_ + ("video: display buffer empty, duping frame (%d/%d)\n"), + decoder->FramesDuped, decoder->FrameCounter); + VaapiPrintFrames(decoder); + } + } + + if (filled) { + pthread_mutex_lock(&VideoLockMutex); + VideoDisplayFrame(); + pthread_mutex_unlock(&VideoLockMutex); + } + } +#if 0 + for (;;) { + int err; + int filled; + struct timespec nowtime; + struct timespec abstime; + VaapiDecoder *decoder; + + clock_gettime(CLOCK_REALTIME, &abstime); + + VideoPollEvent(); + + // fill surface buffer + for (;;) { + static int max_filled; + uint32_t delay; + + clock_gettime(CLOCK_REALTIME, &nowtime); + // time to receive and decode over + if ((nowtime.tv_sec - abstime.tv_sec) * 1000 * 1000 * 1000 + + (nowtime.tv_nsec - abstime.tv_nsec) > + (decoder->Interlaced + 1) * 15 * 1000 * 1000) { + break; + } + + delay = 700 * 1000 * 1000; + // initial delay get decode only 1 frame + if ((nowtime.tv_sec - decoder->StartTime.tv_sec) + * 1000 * 1000 * 1000 + (nowtime.tv_nsec - + decoder->StartTime.tv_nsec) < delay) { + Debug(3, "video/vaapi: waiting %9lu ms\n", + ((nowtime.tv_sec - decoder->StartTime.tv_sec) + * 1000 * 1000 * 1000 + (nowtime.tv_nsec - + decoder->StartTime.tv_nsec)) / (1000 * 1000)); + + if (atomic_read(&decoder->SurfacesFilled)) { + break; + } + } + + if (atomic_read(&decoder->SurfacesFilled) >= 3) { + break; + } + // FIXME: hot polling + pthread_mutex_lock(&VideoLockMutex); + err = VideoDecode(); + pthread_mutex_unlock(&VideoLockMutex); + if (atomic_read(&decoder->SurfacesFilled) > 3) { + Debug(3, "video: %d filled\n", + atomic_read(&decoder->SurfacesFilled)); + if (atomic_read(&decoder->SurfacesFilled) > max_filled) { + max_filled = atomic_read(&decoder->SurfacesFilled); + } + } + if (err) { + usleep(1 * 1000); // nothing buffered + } + + } + + // wait up to 20ms + // FIXME: 50hz video frame rate hardcoded + abstime.tv_nsec += (decoder->Interlaced + 1) * 16 * 1000 * 1000; + if (abstime.tv_nsec >= 1000 * 1000 * 1000) { + // avoid overflow + abstime.tv_sec++; + abstime.tv_nsec -= 1000 * 1000 * 1000; + } + pthread_mutex_lock(&VideoMutex); + while ((err = + pthread_cond_timedwait(&VideoWakeupCond, &VideoMutex, + &abstime)) != ETIMEDOUT) { + Debug(3, "video/vaapi: pthread_cond_timedwait timeout\n"); + } + pthread_mutex_unlock(&VideoMutex); + if (err != ETIMEDOUT) { + Debug(3, "video/vaapi: pthread_cond_timedwait failed: %d\n", err); + } +#ifdef USE_GLX + //printf("video %p <-> %p\n", glXGetCurrentContext(), GlxThreadContext); + if (!glXMakeCurrent(XlibDisplay, VideoWindow, GlxThreadContext)) { + GlxCheck(); + Error(_("video/glx: can't make glx context current\n")); + return NULL; + } +#endif + + filled = atomic_read(&decoder->SurfacesFilled); + if (!filled) { + pthread_mutex_lock(&VideoLockMutex); + VaapiBlackSurface(decoder); + pthread_mutex_unlock(&VideoLockMutex); + } else if (filled == 1) { + decoder->FramesDuped++; + ++decoder->FrameCounter; + Warning(_("video: display buffer empty, duping frame (%d/%d)\n"), + decoder->FramesDuped, decoder->FrameCounter); + if (!(decoder->FrameCounter % 333)) { + VaapiPrintFrames(decoder); + } + } + + if (filled) { + pthread_mutex_lock(&VideoLockMutex); + VideoDisplayFrame(); + pthread_mutex_unlock(&VideoLockMutex); + } + + if (0) { + clock_gettime(CLOCK_REALTIME, &nowtime); + Debug(3, "video/vaapi: ticks %9lu ms\n", + ((nowtime.tv_sec - abstime.tv_sec) * 1000 * 1000 * 1000 + + (nowtime.tv_nsec - abstime.tv_nsec)) / (1000 * 1000)); + } + } +#endif + + return dummy; +} + +/** +** Video render. +*/ +void VideoDisplayHandler(void) +{ + if (!XlibDisplay) { // not yet started + return; + } +#ifdef USE_GLX + glFinish(); // wait for all execution finished + Debug(3, "video: %p <-> %p\n", glXGetCurrentContext(), GlxContext); +#endif + + if (!VideoThread) { +#ifdef USE_GLX + if (GlxEnabled) { // other thread renders + // glXMakeCurrent(XlibDisplay, None, NULL); + } +#endif + + pthread_mutex_init(&VideoMutex, NULL); + pthread_mutex_init(&VideoLockMutex, NULL); + pthread_cond_init(&VideoWakeupCond, NULL); + pthread_create(&VideoThread, NULL, VideoDisplayHandlerThread, NULL); + pthread_detach(VideoThread); + } +} + +/** +** Exit and cleanup video threads. +*/ +static void VideoThreadExit(void) +{ + void *retval; + + if (VideoThread) { + if (pthread_cancel(VideoThread)) { + Error(_("video: can't cancel video display thread\n")); + } + if (pthread_join(VideoThread, &retval) || retval != PTHREAD_CANCELED) { + Error(_("video: can't cancel video display thread\n")); + } + pthread_cond_destroy(&VideoWakeupCond); + pthread_mutex_destroy(&VideoLockMutex); + pthread_mutex_destroy(&VideoMutex); + } +} + +#endif + +//---------------------------------------------------------------------------- +// Video API +//---------------------------------------------------------------------------- + +/// +/// Video hardware decoder +/// +struct _video_hw_decoder_ +{ + union + { +#ifdef USE_VAAPI + VaapiDecoder Vaapi; ///< VA-API decoder structure +#endif +#ifdef USE_VDPAU + VdpauDecoder Vdpau; ///< vdpau decoder structure +#endif + }; +}; + +/// +/// Allocate new video hw decoder. +/// +VideoHwDecoder *VideoNewHwDecoder(void) +{ + if (!XlibDisplay) { // waiting for x11 start + return NULL; + } +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + return (VideoHwDecoder *) VaapiNewDecoder(); + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + return (VideoHwDecoder *) VdpauNewDecoder(); + } +#endif + return NULL; +} + +/// +/// Get a free hardware decoder surface. +/// +/// @param decoder video hardware decoder +/// +unsigned VideoGetSurface(VideoHwDecoder * decoder) +{ +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + return VaapiGetSurface(&decoder->Vaapi); + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + return VdpauGetSurface(&decoder->Vdpau); + } +#endif + return -1; +} + +/// +/// Release a hardware decoder surface. +/// +/// @param decoder video hardware decoder +/// @param surface surface no longer used +/// +void VideoReleaseSurface(VideoHwDecoder * decoder, unsigned surface) +{ +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiReleaseSurface(&decoder->Vaapi, surface); + return; + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + return VdpauReleaseSurface(&decoder->Vdpau, surface); + } +#endif +} + +/// +/// Callback to negotiate the PixelFormat. +/// +/// @param fmt is the list of formats which are supported by the codec, +/// it is terminated by -1 as 0 is a valid format, the +/// formats are ordered by quality. +/// +enum PixelFormat Video_get_format(VideoHwDecoder * decoder, + AVCodecContext * video_ctx, const enum PixelFormat *fmt) +{ +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + return Vaapi_get_format(&decoder->Vaapi, video_ctx, fmt); + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + return Vdpau_get_format(&decoder->Vdpau, video_ctx, fmt); + } +#endif + return fmt[0]; +} + +/** +** Test +*/ +void VaapiTest(void) +{ + static int state; + static uint32_t clock; + static uint32_t last_tick; + int i; + + //XLockDisplay(XlibDisplay); + VideoPollEvent(); + for (i = 0; i < VaapiDecoderN; ++i) { + int filled; + VaapiDecoder *decoder; + uint32_t start; + uint32_t end; + + decoder = VaapiDecoders[i]; + filled = atomic_read(&decoder->SurfacesFilled); + if (!filled) { // trick to reset for new streams + state = 0; + } + switch (state) { + case 0: + // new stream, wait until enough frames are buffered + Debug(3, "video/state: wait on full\n"); + if (filled == 1) { + VaapiDisplayFrame(); + } + if (filled < VIDEO_SURFACES_MAX - 1) { + continue; + } + state++; + case 1: + // we have enough frames buffered, fill driver buffer + Debug(3, "video/state: ringbuffer full\n"); + // intel has 0 buffers + //VaapiDisplayFrame(); + state++; + case 2: + // normal run, just play a buffered frame + start = GetMsTicks(); + // intel 20ms / 40ms + VaapiDisplayFrame(); + end = GetMsTicks(); + last_tick = end; + if (start + (decoder->Interlaced + 1) * 20 < end) { + Debug(3, "video/state: display %u ms\n", end - start); + } + clock += (decoder->Interlaced + 1) * 20; + if (last_tick < clock - 1000) { + clock = last_tick; + } + if (last_tick > clock + 1000) { + clock = last_tick; + } + //Debug(3, "video/state: %+4d ms\n", clock - last_tick); + break; + } + } + //XUnlockDisplay(XlibDisplay); +} + +/// +/// Display a ffmpeg frame +/// +/// @param decoder video hardware decoder +/// @param video_ctx ffmpeg video codec context +/// @param frame frame to display +/// +void VideoRenderFrame(VideoHwDecoder * decoder, AVCodecContext * video_ctx, + AVFrame * frame) +{ + if (!atomic_read(&decoder->Vaapi.SurfacesFilled)) { + Debug(3, "video: new stream frame %d\n", GetMsTicks() - VideoSwitch); + } + // if video output buffer is full, wait and display surface. + if (atomic_read(&decoder->Vaapi.SurfacesFilled) >= VIDEO_SURFACES_MAX) { + struct timespec abstime; + + abstime = decoder->Vaapi.FrameTime; + abstime.tv_nsec += 16 * 1000 * 1000; + if (abstime.tv_nsec >= 1000 * 1000 * 1000) { + // avoid overflow + abstime.tv_sec++; + abstime.tv_nsec -= 1000 * 1000 * 1000; + } + + VideoPollEvent(); + + // give osd some time slot + while (pthread_cond_timedwait(&VideoWakeupCond, &VideoLockMutex, + &abstime) != ETIMEDOUT) { + // SIGUSR1 + Debug(3, "video/vaapi: pthread_cond_timedwait error\n"); + } + + VideoDisplayFrame(); + } +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiRenderFrame(&decoder->Vaapi, video_ctx, frame); + return; + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + VdpauRenderFrame(&decoder->Vdpau, video_ctx, frame); + return; + } +#endif + (void)decoder; + (void)video_ctx; + (void)frame; + //Error(_("video: unsupported %p %p %p\n"), decoder, video_ctx, frame); +} + +/// +/// Get VA-API ffmpeg context +/// +/// @param decoder VA-API decoder +/// +struct vaapi_context *VideoGetVaapiContext(VideoHwDecoder * decoder) +{ +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + return decoder->Vaapi.VaapiContext; + } +#endif + Error(_("video/vaapi: get vaapi context, without vaapi enabled\n")); + return NULL; +} + +#ifndef USE_VIDEO_THREAD + +/** +** Video render. +*/ +void VideoDisplayHandler(void) +{ + uint32_t now; + + if (!XlibDisplay) { // not yet started + return; + } + + now = GetMsTicks(); + if (now < VaapiDecoders[0]->LastFrameTick) { + return; + } + if (now - VaapiDecoders[0]->LastFrameTick < 500) { + return; + } + VideoPollEvent(); + VaapiBlackSurface(VaapiDecoders[0]); + + return; +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VaapiDisplayFrame(); + return; + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + return; + } +#endif + VideoDisplayFrame(); +} + +#endif + +//---------------------------------------------------------------------------- +// Setup +//---------------------------------------------------------------------------- + +/** +** Create main window. +*/ +static void VideoCreateWindow(xcb_window_t parent, xcb_visualid_t visual, + uint8_t depth) +{ + uint32_t values[4]; + xcb_intern_atom_reply_t *reply; + + Debug(3, "video: visual %#0x depth %d\n", visual, depth); + + // Color map + VideoColormap = xcb_generate_id(Connection); + xcb_create_colormap(Connection, XCB_COLORMAP_ALLOC_NONE, VideoColormap, + parent, visual); + + values[0] = 0; + values[1] = 0; + values[2] = + XCB_EVENT_MASK_KEY_PRESS | XCB_EVENT_MASK_KEY_RELEASE | + XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_BUTTON_RELEASE | + XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_STRUCTURE_NOTIFY; + values[3] = VideoColormap; + VideoWindow = xcb_generate_id(Connection); + xcb_create_window(Connection, depth, VideoWindow, parent, VideoWindowX, + VideoWindowY, VideoWindowWidth, VideoWindowHeight, 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, visual, + XCB_CW_BACK_PIXEL | XCB_CW_BORDER_PIXEL | XCB_CW_EVENT_MASK | + XCB_CW_COLORMAP, values); + + // FIXME: utf _NET_WM_NAME + xcb_icccm_set_wm_name(Connection, VideoWindow, XCB_ATOM_STRING, 8, + sizeof("softhddevice") - 1, "softhddevice"); + xcb_icccm_set_wm_icon_name(Connection, VideoWindow, XCB_ATOM_STRING, 8, + sizeof("softhddevice") - 1, "softhddevice"); + + // FIXME: size hints + + // register interest in the delete window message + if ((reply = + xcb_intern_atom_reply(Connection, xcb_intern_atom(Connection, 0, + sizeof("WM_DELETE_WINDOW") - 1, "WM_DELETE_WINDOW"), + NULL))) { + WmDeleteWindowAtom = reply->atom; + free(reply); + if ((reply = + xcb_intern_atom_reply(Connection, xcb_intern_atom(Connection, + 0, sizeof("WM_PROTOCOLS") - 1, "WM_PROTOCOLS"), + NULL))) { + xcb_icccm_set_wm_protocols(Connection, VideoWindow, reply->atom, 1, + &WmDeleteWindowAtom); + free(reply); + } + } + + values[0] = XCB_NONE; + xcb_change_window_attributes(Connection, VideoWindow, XCB_CW_CURSOR, + values); + + xcb_map_window(Connection, VideoWindow); +} + +/** +** Set video geometry. +** +** @param geometry [=][{xX}][{+-}{+-}] +*/ +int VideoSetGeometry(const char *geometry) +{ + int flags; + + flags = + XParseGeometry(geometry, &VideoWindowX, &VideoWindowY, + &VideoWindowWidth, &VideoWindowHeight); + + return 0; +} + +/** +** Initialize video output module. +** +** @param display_name X11 display name +*/ +void VideoInit(const char *display_name) +{ + int screen_nr; + int i; + xcb_screen_iterator_t screen_iter; + xcb_screen_t *screen; + + if (XlibDisplay) { // allow multiple calls + Debug(3, "video: x11 already setup\n"); + return; + } + // Open the connection to the X server. + // use the DISPLAY environment variable as the default display name + if (!display_name) { + display_name = getenv("DISPLAY"); + if (!display_name) { + // use :0.0 as default display name + display_name = ":0.0"; + } + } + if (!(XlibDisplay = XOpenDisplay(display_name))) { + Fatal(_("video: Can't connect to X11 server on '%s'"), display_name); + // FIXME: we need to retry connection + } + XInitThreads(); + // Convert XLIB display to XCB connection + if (!(Connection = XGetXCBConnection(XlibDisplay))) { + Fatal(_("video: Can't convert XLIB display to XCB connection")); + } + // prefetch extensions + //xcb_prefetch_extension_data(Connection, &xcb_big_requests_id); + //xcb_prefetch_extension_data(Connection, &xcb_dpms_id); + //xcb_prefetch_extension_data(Connection, &xcb_glx_id); + //xcb_prefetch_extension_data(Connection, &xcb_randr_id); + //xcb_prefetch_extension_data(Connection, &xcb_screensaver_id); + //xcb_prefetch_extension_data(Connection, &xcb_shm_id); + //xcb_prefetch_extension_data(Connection, &xcb_xv_id); + + // Get the requested screen number + screen_nr = DefaultScreen(XlibDisplay); + screen_iter = xcb_setup_roots_iterator(xcb_get_setup(Connection)); + for (i = 0; i < screen_nr; ++i) { + xcb_screen_next(&screen_iter); + } + screen = screen_iter.data; + + // + // Default window size + // + if (!VideoWindowHeight) { + if (VideoWindowWidth) { + VideoWindowHeight = (VideoWindowWidth * 9) / 16; + } + VideoWindowHeight = 576; + } + if (!VideoWindowWidth) { + VideoWindowWidth = (VideoWindowHeight * 16) / 9; + } + // + // prepare opengl + // +#ifdef USE_GLX + if (GlxEnabled) { + + GlxInit(); + // FIXME: use root window? + VideoCreateWindow(screen->root, GlxVisualInfo->visualid, + GlxVisualInfo->depth); + GlxSetupWindow(VideoWindow, VideoWindowWidth, VideoWindowHeight); + } else +#endif + + // + // Create output window + // + if (1) { // FIXME: use window mode + + VideoCreateWindow(screen->root, screen->root_visual, + screen->root_depth); + + } else { + // FIXME: support embedded mode + VideoWindow = screen->root; + + // FIXME: VideoWindowHeight VideoWindowWidth + } + + Debug(3, "video: window prepared\n"); + + // + // prepare hardware decoder VA-API/VDPAU + // +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VideoVaapiInit(display_name); + } +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + VideoVdpauInit(display_name); + } +#endif + + //xcb_prefetch_maximum_request_length(Connection); + xcb_flush(Connection); +} + +/** +** Cleanup video output module. +*/ +void VideoExit(void) +{ + if (!XlibDisplay) { // no init or failed + return; + } +#ifdef USE_VIDEO_THREAD + VideoThreadExit(); +#endif +#ifdef USE_VDPAU + if (VideoVdpauEnabled) { + VideoVdpauExit(); + } +#endif +#ifdef USE_VAAPI + if (VideoVaapiEnabled) { + VideoVaapiExit(); + } +#endif +#ifdef USE_GLX + if (GlxEnabled) { + GlxExit(); + } +#endif + + // + // Reenable screensaver / DPMS. + // + //X11SuspendScreenSaver(XlibDisplay, False); + //X11DPMSEnable(XlibDisplay); + + // + // FIXME: cleanup. + // + //RandrExit(); +} + +#endif + +#ifdef VIDEO_TEST + +#include + +int SysLogLevel; ///< show additional debug informations + +/** +** Print version. +*/ +static void PrintVersion(void) +{ + printf("video_test: video tester Version " VERSION +#ifdef GIT_REV + "(GIT-" GIT_REV ")" +#endif + ",\n\t(c) 2009 - 2011 by Johns\n" + "\tLicense AGPLv3: GNU Affero General Public License version 3\n"); +} + +/** +** Print usage. +*/ +static void PrintUsage(void) +{ + printf("Usage: video_test [-?dhv]\n" + "\t-d\tenable debug, more -d increase the verbosity\n" + "\t-? -h\tdisplay this message\n" "\t-v\tdisplay version information\n" + "Only idiots print usage on stderr!\n"); +} + +/** +** Main entry point. +** +** @param argc number of arguments +** @param argv arguments vector +** +** @returns -1 on failures, 0 clean exit. +*/ +int main(int argc, char *const argv[]) +{ + SysLogLevel = 0; + + // + // Parse command line arguments + // + for (;;) { + switch (getopt(argc, argv, "hv?-c:d")) { + case 'd': // enabled debug + ++SysLogLevel; + continue; + + case EOF: + break; + case 'v': // print version + PrintVersion(); + return 0; + case '?': + case 'h': // help usage + PrintVersion(); + PrintUsage(); + return 0; + case '-': + PrintVersion(); + PrintUsage(); + fprintf(stderr, "\nWe need no long options\n"); + return -1; + case ':': + PrintVersion(); + fprintf(stderr, "Missing argument for option '%c'\n", optopt); + return -1; + default: + PrintVersion(); + fprintf(stderr, "Unkown option '%c'\n", optopt); + return -1; + } + break; + } + if (optind < argc) { + PrintVersion(); + while (optind < argc) { + fprintf(stderr, "Unhandled argument '%s'\n", argv[optind++]); + } + return -1; + } + // + // main loop + // + VideoInit(); + VideoOsdInit(); + for (;;) { + VideoRenderOverlay(); + VideoDisplayOverlay(); + glXSwapBuffers(XlibDisplay, VideoWindow); + GlxCheck(); + glClear(GL_COLOR_BUFFER_BIT); + + XFlush(XlibDisplay); + XSync(XlibDisplay, False); + XFlush(XlibDisplay); + XSync(XlibDisplay, False); + XFlush(XlibDisplay); + XSync(XlibDisplay, False); + XFlush(XlibDisplay); + XSync(XlibDisplay, False); + XFlush(XlibDisplay); + XSync(XlibDisplay, False); + XFlush(XlibDisplay); + usleep(20 * 1000); + } + VideoExit(); + + return 0; +} + +#endif diff --git a/video.h b/video.h new file mode 100644 index 0000000..73946d0 --- /dev/null +++ b/video.h @@ -0,0 +1,89 @@ +/// +/// @file video.h @brief Video module header file +/// +/// Copyright (c) 2009 - 2011 by Johns. All Rights Reserved. +/// +/// Contributor(s): +/// +/// License: AGPLv3 +/// +/// This program is free software: you can redistribute it and/or modify +/// it under the terms of the GNU Affero General Public License as +/// published by the Free Software Foundation, either version 3 of the +/// License. +/// +/// This program 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 Affero General Public License for more details. +/// +/// $Id$ +////////////////////////////////////////////////////////////////////////////// + +/// @addtogroup Video +/// @{ + +//---------------------------------------------------------------------------- +// Typedefs +//---------------------------------------------------------------------------- + + /// Video hardware decoder typedef +typedef struct _video_hw_decoder_ VideoHwDecoder; + +//---------------------------------------------------------------------------- +// Variables +//---------------------------------------------------------------------------- + +//extern unsigned VideoWindowWidth; ///< current video output width +//extern unsigned VideoWindowHeight; ///< current video output height + +//---------------------------------------------------------------------------- +// Prototypes +//---------------------------------------------------------------------------- + + /// Allocate new video hardware decoder. +extern VideoHwDecoder *VideoNewHwDecoder(void); + + /// Get and allocate a video hardware surface. +extern unsigned VideoGetSurface(VideoHwDecoder *); + + /// Release a video hardware surface. +extern void VideoReleaseSurface(VideoHwDecoder *, unsigned); + +#ifdef LIBAVCODEC_VERSION + /// Render a ffmpeg frame +extern void VideoRenderFrame(VideoHwDecoder *, AVCodecContext *, AVFrame *); + + /// Get ffmpeg vaapi context +extern struct vaapi_context *VideoGetVaapiContext(VideoHwDecoder *); + + /// Callback to negotiate the PixelFormat. +extern enum PixelFormat Video_get_format(VideoHwDecoder *, AVCodecContext *, + const enum PixelFormat *); +#endif + + /// Display video TEST +extern void VideoDisplayHandler(void); + + /// set video mode +//extern void VideoSetVideoMode(int, int, int, int); + + /// set video geometry +extern int VideoSetGeometry(const char *); + + /// Clear OSD +extern void VideoOsdClear(void); + + /// Draw an OSD ARGB image +extern void VideoOsdDrawARGB(int, int, int, int, const uint8_t *); + +extern void VideoOsdInit(void); ///< setup osd +extern void VideoOsdExit(void); ///< cleanup osd + +extern void VideoInit(const char *); ///< setup video module +extern void VideoExit(void); ///< cleanup and exit video module + +extern void VideoFlushInput(void); ///< flush codec input buffers +extern int VideoDecode(void); ///< decode + +/// @}