1186 lines
35 KiB
C
1186 lines
35 KiB
C
/*
|
|
* Copyright © 2018 Benjamin Otte
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Lesser General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2.1 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Lesser General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Lesser General Public
|
|
* License along with this library. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
* Authors: Benjamin Otte <otte@gnome.org>
|
|
*/
|
|
|
|
#include "config.h"
|
|
|
|
#include "gtkffmediafileprivate.h"
|
|
|
|
#include <glib/gi18n-lib.h>
|
|
|
|
#include "gdk/gdkmemorytextureprivate.h"
|
|
|
|
#include <libavcodec/avcodec.h>
|
|
#include <libavdevice/avdevice.h>
|
|
#include <libavformat/avformat.h>
|
|
#include <libavutil/channel_layout.h>
|
|
#include <libavutil/pixdesc.h>
|
|
#include <libswresample/swresample.h>
|
|
#include <libswscale/swscale.h>
|
|
|
|
typedef struct _GtkVideoFrameFFMpeg GtkVideoFrameFFMpeg;
|
|
|
|
struct _GtkVideoFrameFFMpeg
|
|
{
|
|
GdkTexture *texture;
|
|
gint64 timestamp;
|
|
};
|
|
|
|
typedef struct _GtkFStream GtkFfStream;
|
|
|
|
struct _GtkFStream
|
|
{
|
|
AVCodecContext *codec_ctx;
|
|
AVStream *stream;
|
|
int stream_id;
|
|
int type;
|
|
};
|
|
|
|
struct _GtkFfMediaFile
|
|
{
|
|
GtkMediaFile parent_instance;
|
|
|
|
GFile *file;
|
|
GInputStream *input_stream;
|
|
|
|
AVFormatContext *device_ctx; /* used for avdevice audio playback */
|
|
AVFormatContext *format_ctx;
|
|
|
|
GtkFfStream *input_audio_stream;
|
|
GtkFfStream *input_video_stream;
|
|
|
|
GtkFfStream *output_audio_stream;
|
|
|
|
gint64 audio_samples_count;
|
|
|
|
// Resampling
|
|
struct SwrContext *swr_ctx;
|
|
AVFrame* audio_frame;
|
|
|
|
// Rescaling
|
|
struct SwsContext *sws_ctx;
|
|
enum AVPixelFormat sws_pix_fmt;
|
|
GdkMemoryFormat memory_format;
|
|
|
|
GtkVideoFrameFFMpeg current_frame;
|
|
GtkVideoFrameFFMpeg next_frame;
|
|
|
|
gint64 start_time; /* monotonic time when we displayed the last frame */
|
|
guint next_frame_cb; /* Source ID of next frame callback */
|
|
};
|
|
|
|
struct _GtkFfMediaFileClass
|
|
{
|
|
GtkMediaFileClass parent_class;
|
|
};
|
|
|
|
static void
|
|
gtk_video_frame_ffmpeg_init (GtkVideoFrameFFMpeg *frame,
|
|
GdkTexture *texture,
|
|
gint64 timestamp)
|
|
{
|
|
frame->texture = texture;
|
|
frame->timestamp = timestamp;
|
|
}
|
|
|
|
static void
|
|
gtk_video_frame_ffmpeg_clear (GtkVideoFrameFFMpeg *frame)
|
|
{
|
|
g_clear_object (&frame->texture);
|
|
frame->timestamp = 0;
|
|
}
|
|
|
|
static gboolean
|
|
gtk_video_frame_ffmpeg_is_empty (GtkVideoFrameFFMpeg *frame)
|
|
{
|
|
return frame->texture == NULL;
|
|
}
|
|
|
|
static void
|
|
gtk_video_frame_ffmpeg_move (GtkVideoFrameFFMpeg *dest,
|
|
GtkVideoFrameFFMpeg *src)
|
|
{
|
|
*dest = *src;
|
|
src->texture = NULL;
|
|
src->timestamp = 0;
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_paintable_snapshot (GdkPaintable *paintable,
|
|
GdkSnapshot *snapshot,
|
|
double width,
|
|
double height)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (paintable);
|
|
|
|
if (!gtk_video_frame_ffmpeg_is_empty (&self->current_frame))
|
|
{
|
|
gdk_paintable_snapshot (GDK_PAINTABLE (self->current_frame.texture), snapshot, width, height);
|
|
}
|
|
}
|
|
|
|
static GdkPaintable *
|
|
gtk_ff_media_file_paintable_get_current_image (GdkPaintable *paintable)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (paintable);
|
|
|
|
if (gtk_video_frame_ffmpeg_is_empty (&self->current_frame))
|
|
{
|
|
if (self->input_video_stream->codec_ctx)
|
|
return gdk_paintable_new_empty (self->input_video_stream->codec_ctx->width, self->input_video_stream->codec_ctx->height);
|
|
else
|
|
return gdk_paintable_new_empty (0, 0);
|
|
}
|
|
|
|
return GDK_PAINTABLE (g_object_ref (self->current_frame.texture));
|
|
}
|
|
|
|
static int
|
|
gtk_ff_media_file_paintable_get_intrinsic_width (GdkPaintable *paintable)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (paintable);
|
|
|
|
if (self->input_video_stream->codec_ctx)
|
|
return self->input_video_stream->codec_ctx->width;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int
|
|
gtk_ff_media_file_paintable_get_intrinsic_height (GdkPaintable *paintable)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (paintable);
|
|
|
|
if (self->input_video_stream->codec_ctx)
|
|
return self->input_video_stream->codec_ctx->height;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static double gtk_ff_media_file_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (paintable);
|
|
|
|
if (self->input_video_stream->codec_ctx)
|
|
return (double) self->input_video_stream->codec_ctx->width / self->input_video_stream->codec_ctx->height;
|
|
|
|
return 0.0;
|
|
};
|
|
|
|
static void
|
|
gtk_ff_media_file_paintable_init (GdkPaintableInterface *iface)
|
|
{
|
|
iface->snapshot = gtk_ff_media_file_paintable_snapshot;
|
|
iface->get_current_image = gtk_ff_media_file_paintable_get_current_image;
|
|
iface->get_intrinsic_width = gtk_ff_media_file_paintable_get_intrinsic_width;
|
|
iface->get_intrinsic_height = gtk_ff_media_file_paintable_get_intrinsic_height;
|
|
iface->get_intrinsic_aspect_ratio = gtk_ff_media_file_paintable_get_intrinsic_aspect_ratio;
|
|
}
|
|
|
|
G_DEFINE_TYPE_EXTENDED (GtkFfMediaFile, gtk_ff_media_file, GTK_TYPE_MEDIA_FILE, 0,
|
|
G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE,
|
|
gtk_ff_media_file_paintable_init))
|
|
|
|
G_MODULE_EXPORT
|
|
void
|
|
g_io_module_load (GIOModule *module)
|
|
{
|
|
g_type_module_use (G_TYPE_MODULE (module));
|
|
|
|
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT (58, 9, 100)
|
|
av_register_all ();
|
|
#endif
|
|
|
|
g_io_extension_point_implement (GTK_MEDIA_FILE_EXTENSION_POINT_NAME,
|
|
GTK_TYPE_FF_MEDIA_FILE,
|
|
"ffmpeg",
|
|
0);
|
|
}
|
|
|
|
G_MODULE_EXPORT
|
|
G_GNUC_NORETURN
|
|
void
|
|
g_io_module_unload (GIOModule *module)
|
|
{
|
|
g_assert_not_reached ();
|
|
}
|
|
|
|
G_MODULE_EXPORT
|
|
char **
|
|
g_io_module_query (void)
|
|
{
|
|
char *eps[] = {
|
|
(char *) GTK_MEDIA_FILE_EXTENSION_POINT_NAME,
|
|
NULL
|
|
};
|
|
|
|
return g_strdupv (eps);
|
|
}
|
|
|
|
static void
|
|
gtk_ff_stream_close (GtkFfStream *stream)
|
|
{
|
|
stream->stream_id = -1;
|
|
g_clear_pointer (&stream->codec_ctx, avcodec_close);
|
|
g_free (stream);
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_set_ffmpeg_error (GtkFfMediaFile *self,
|
|
int av_errnum)
|
|
{
|
|
char s[AV_ERROR_MAX_STRING_SIZE];
|
|
|
|
if (gtk_media_stream_get_error (GTK_MEDIA_STREAM (self)))
|
|
return;
|
|
|
|
if (av_strerror (av_errnum, s, sizeof (s) != 0))
|
|
g_snprintf (s, sizeof (s), _("Unspecified error decoding media"));
|
|
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
"%s",
|
|
s);
|
|
}
|
|
|
|
static GtkFfStream *
|
|
gtk_ff_media_file_find_input_stream (GtkFfMediaFile *self,
|
|
int type)
|
|
{
|
|
GtkFfStream *ff_stream;
|
|
const AVCodec *codec;
|
|
AVCodecContext *codec_ctx;
|
|
AVStream *stream;
|
|
int stream_id;
|
|
int errnum;
|
|
|
|
stream_id = av_find_best_stream (self->format_ctx, type, -1, -1, NULL, 0);
|
|
if (stream_id < 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
stream = self->format_ctx->streams[stream_id];
|
|
codec = avcodec_find_decoder (stream->codecpar->codec_id);
|
|
if (codec == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Cannot find decoder: %s"),
|
|
avcodec_get_name (stream->codecpar->codec_id));
|
|
return NULL;
|
|
}
|
|
codec_ctx = avcodec_alloc_context3 (codec);
|
|
if (codec_ctx == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Failed to allocate a codec context"));
|
|
return NULL;
|
|
}
|
|
errnum = avcodec_parameters_to_context (codec_ctx, stream->codecpar);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
avcodec_close (codec_ctx);
|
|
return NULL;
|
|
}
|
|
errnum = avcodec_open2 (codec_ctx, codec, &stream->metadata);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
avcodec_close (codec_ctx);
|
|
return NULL;
|
|
}
|
|
|
|
ff_stream = g_new (GtkFfStream, 1);
|
|
ff_stream->codec_ctx = codec_ctx;
|
|
ff_stream->stream = stream;
|
|
ff_stream->stream_id = stream_id;
|
|
ff_stream->type = type;
|
|
return ff_stream;
|
|
}
|
|
|
|
static GtkFfStream *
|
|
gtk_ff_media_file_add_output_stream (GtkFfMediaFile *self,
|
|
AVFormatContext *fmt_ctx,
|
|
enum AVCodecID codec_id)
|
|
{
|
|
GtkFfStream *ff_media_stream;
|
|
const AVCodec *codec;
|
|
AVCodecContext *codec_ctx;
|
|
AVStream *stream;
|
|
int stream_id;
|
|
int errnum;
|
|
|
|
// find the encoder
|
|
codec = avcodec_find_encoder (codec_id);
|
|
if (codec == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Cannot find encoder: %s"),
|
|
avcodec_get_name (codec_id));
|
|
return NULL;
|
|
}
|
|
|
|
stream = avformat_new_stream (fmt_ctx, NULL);
|
|
if (stream == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Cannot add new stream"));
|
|
return NULL;
|
|
}
|
|
stream_id = fmt_ctx->nb_streams - 1;
|
|
|
|
codec_ctx = avcodec_alloc_context3 (codec);
|
|
if (codec_ctx == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Failed to allocate a codec context"));
|
|
return NULL;
|
|
}
|
|
|
|
// set the encoder options
|
|
codec_ctx->sample_fmt = codec->sample_fmts ? codec->sample_fmts[0] : AV_SAMPLE_FMT_S16;
|
|
codec_ctx->sample_rate = codec->supported_samplerates ? codec->supported_samplerates[0] : 48000;
|
|
codec_ctx->channel_layout = codec->channel_layouts ? codec->channel_layouts[0] : AV_CH_LAYOUT_STEREO;
|
|
codec_ctx->channels = av_get_channel_layout_nb_channels (codec_ctx->channel_layout);
|
|
|
|
stream->time_base = (AVRational){ 1, codec_ctx->sample_rate };
|
|
|
|
// open the codec
|
|
errnum = avcodec_open2 (codec_ctx, codec, NULL);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
avcodec_close (codec_ctx);
|
|
return NULL;
|
|
}
|
|
|
|
errnum = avcodec_parameters_from_context (stream->codecpar, codec_ctx);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
avcodec_close (codec_ctx);
|
|
return NULL;
|
|
}
|
|
|
|
ff_media_stream = g_new (GtkFfStream, 1);
|
|
ff_media_stream->codec_ctx = codec_ctx;
|
|
ff_media_stream->stream = stream;
|
|
ff_media_stream->stream_id = stream_id;
|
|
ff_media_stream->type = AVMEDIA_TYPE_AUDIO;
|
|
return ff_media_stream;
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_seek_stream (GtkFfMediaFile *self, GtkFfStream *stream, int64_t timestamp)
|
|
{
|
|
int errnum;
|
|
|
|
if (!stream)
|
|
return TRUE;
|
|
|
|
errnum = av_seek_frame (self->format_ctx,
|
|
stream->stream_id,
|
|
av_rescale_q (timestamp,
|
|
(AVRational){ 1, G_USEC_PER_SEC },
|
|
stream->stream->time_base),
|
|
AVSEEK_FLAG_BACKWARD);
|
|
|
|
if (errnum < 0)
|
|
{
|
|
gtk_media_stream_seek_failed (GTK_MEDIA_STREAM (self));
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static AVFrame *
|
|
gtk_ff_media_file_alloc_audio_frame (enum AVSampleFormat sample_fmt,
|
|
uint64_t channel_layout,
|
|
int sample_rate,
|
|
int nb_samples)
|
|
{
|
|
AVFrame *frame = av_frame_alloc ();
|
|
int ret;
|
|
|
|
if (!frame)
|
|
{
|
|
return NULL;
|
|
}
|
|
|
|
frame->format = sample_fmt;
|
|
frame->channel_layout = channel_layout;
|
|
frame->sample_rate = sample_rate;
|
|
frame->nb_samples = nb_samples;
|
|
|
|
if (nb_samples)
|
|
{
|
|
ret = av_frame_get_buffer (frame, 0);
|
|
if (ret < 0)
|
|
{
|
|
return NULL;
|
|
}
|
|
}
|
|
|
|
return frame;
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_write_audio_frame (GtkFfMediaFile *self, AVFrame* frame)
|
|
{
|
|
AVFormatContext *device_ctx;
|
|
AVCodecContext *codec_ctx;
|
|
AVStream *stream;
|
|
AVFrame *resampled_frame;
|
|
int errnum;
|
|
int dst_nb_samples;
|
|
|
|
device_ctx = self->device_ctx;
|
|
codec_ctx = self->output_audio_stream->codec_ctx;
|
|
stream = self->output_audio_stream->stream;
|
|
|
|
if (frame)
|
|
{
|
|
dst_nb_samples = av_rescale_rnd (swr_get_delay (self->swr_ctx, codec_ctx->sample_rate) + frame->nb_samples,
|
|
codec_ctx->sample_rate,
|
|
codec_ctx->sample_rate,
|
|
AV_ROUND_UP);
|
|
|
|
resampled_frame = gtk_ff_media_file_alloc_audio_frame (codec_ctx->sample_fmt,
|
|
codec_ctx->channel_layout,
|
|
codec_ctx->sample_rate,
|
|
dst_nb_samples);
|
|
if (resampled_frame == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Failed to allocate an audio frame"));
|
|
return;
|
|
}
|
|
|
|
errnum = swr_convert (self->swr_ctx,
|
|
resampled_frame->data, dst_nb_samples,
|
|
(const uint8_t **) frame->data, frame->nb_samples);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return;
|
|
}
|
|
frame = resampled_frame;
|
|
|
|
|
|
frame->pts = av_rescale_q (self->audio_samples_count,
|
|
(AVRational){ 1, codec_ctx->sample_rate },
|
|
codec_ctx->time_base);
|
|
|
|
errnum = av_write_uncoded_frame (device_ctx, stream->index, frame);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return;
|
|
}
|
|
|
|
self->audio_samples_count += frame->nb_samples;
|
|
}
|
|
}
|
|
|
|
static int
|
|
gtk_ff_media_file_read_packet_cb (void *data,
|
|
uint8_t *buf,
|
|
int buf_size)
|
|
{
|
|
GtkFfMediaFile *self = data;
|
|
GError *error = NULL;
|
|
gssize n_read;
|
|
|
|
n_read = g_input_stream_read (self->input_stream,
|
|
buf,
|
|
buf_size,
|
|
NULL,
|
|
&error);
|
|
if (n_read < 0)
|
|
{
|
|
gtk_media_stream_gerror (GTK_MEDIA_STREAM (self), error);
|
|
}
|
|
else if (n_read == 0)
|
|
{
|
|
n_read = AVERROR_EOF;
|
|
}
|
|
|
|
return n_read;
|
|
}
|
|
|
|
static GdkMemoryFormat
|
|
memory_format_from_pix_fmt (enum AVPixelFormat pix_fmt)
|
|
{
|
|
switch ((int) pix_fmt)
|
|
{
|
|
case AV_PIX_FMT_RGBA:
|
|
return GDK_MEMORY_R8G8B8A8;
|
|
case AV_PIX_FMT_RGB24:
|
|
return GDK_MEMORY_R8G8B8;
|
|
default:
|
|
g_assert_not_reached ();
|
|
return GDK_MEMORY_R8G8B8A8;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_decode_frame (GtkFfMediaFile *self,
|
|
GtkVideoFrameFFMpeg *result)
|
|
{
|
|
GdkTexture *texture;
|
|
AVPacket packet;
|
|
AVFrame *frame;
|
|
int errnum;
|
|
GBytes *bytes;
|
|
guchar *data;
|
|
|
|
frame = av_frame_alloc ();
|
|
|
|
for (errnum = av_read_frame (self->format_ctx, &packet);
|
|
errnum >= 0;
|
|
errnum = av_read_frame (self->format_ctx, &packet))
|
|
{
|
|
if (self->input_audio_stream && packet.stream_index == self->input_audio_stream->stream_id)
|
|
{
|
|
errnum = avcodec_send_packet (self->input_audio_stream->codec_ctx, &packet);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return FALSE;
|
|
}
|
|
if (errnum >= 0)
|
|
{
|
|
errnum = avcodec_receive_frame (self->input_audio_stream->codec_ctx, self->audio_frame);
|
|
if (errnum == AVERROR (EAGAIN))
|
|
{
|
|
// Just retry with the next packet
|
|
errnum = 0;
|
|
continue;
|
|
}
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
av_packet_unref (&packet);
|
|
}
|
|
}
|
|
|
|
gtk_ff_media_file_write_audio_frame(self, self->audio_frame);
|
|
}
|
|
else if (self->input_video_stream && packet.stream_index == self->input_video_stream->stream_id)
|
|
{
|
|
errnum = avcodec_send_packet (self->input_video_stream->codec_ctx, &packet);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return FALSE;
|
|
}
|
|
if (errnum >= 0)
|
|
{
|
|
errnum = avcodec_receive_frame (self->input_video_stream->codec_ctx, frame);
|
|
if (errnum == AVERROR (EAGAIN))
|
|
{
|
|
// Just retry with the next packet
|
|
errnum = 0;
|
|
continue;
|
|
}
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return FALSE;
|
|
}
|
|
else
|
|
{
|
|
av_packet_unref (&packet);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
av_packet_unref (&packet);
|
|
}
|
|
|
|
if (errnum < 0)
|
|
{
|
|
if (errnum != AVERROR_EOF)
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
av_frame_free (&frame);
|
|
return FALSE;
|
|
}
|
|
|
|
data = g_try_malloc0 (self->input_video_stream->codec_ctx->width * self->input_video_stream->codec_ctx->height * 4);
|
|
if (data == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
_("Not enough memory"));
|
|
av_frame_free (&frame);
|
|
return FALSE;
|
|
}
|
|
|
|
if (self->sws_ctx == NULL ||
|
|
self->sws_pix_fmt != frame->format)
|
|
{
|
|
const AVPixFmtDescriptor *desc;
|
|
enum AVPixelFormat gdk_pix_fmt;
|
|
|
|
g_clear_pointer (&self->sws_ctx, sws_freeContext);
|
|
self->sws_pix_fmt = frame->format;
|
|
desc = av_pix_fmt_desc_get (self->sws_pix_fmt);
|
|
/* Use gdk-pixbuf formats because ffmpeg can't premultiply */
|
|
if (desc != NULL && (desc->flags & AV_PIX_FMT_FLAG_ALPHA))
|
|
gdk_pix_fmt = AV_PIX_FMT_RGBA;
|
|
else
|
|
gdk_pix_fmt = AV_PIX_FMT_RGB24;
|
|
|
|
self->sws_ctx = sws_getContext (self->input_video_stream->codec_ctx->width,
|
|
self->input_video_stream->codec_ctx->height,
|
|
frame->format,
|
|
self->input_video_stream->codec_ctx->width,
|
|
self->input_video_stream->codec_ctx->height,
|
|
gdk_pix_fmt,
|
|
0,
|
|
NULL,
|
|
NULL,
|
|
NULL);
|
|
|
|
self->memory_format = memory_format_from_pix_fmt (gdk_pix_fmt);
|
|
}
|
|
|
|
sws_scale(self->sws_ctx,
|
|
(const uint8_t * const *) frame->data, frame->linesize,
|
|
0, self->input_video_stream->codec_ctx->height,
|
|
(uint8_t *[1]) { data }, (int[1]) { self->input_video_stream->codec_ctx->width * 4 });
|
|
|
|
bytes = g_bytes_new_take (data, self->input_video_stream->codec_ctx->width * self->input_video_stream->codec_ctx->height * 4);
|
|
texture = gdk_memory_texture_new (self->input_video_stream->codec_ctx->width,
|
|
self->input_video_stream->codec_ctx->height,
|
|
self->memory_format,
|
|
bytes,
|
|
self->input_video_stream->codec_ctx->width * 4);
|
|
|
|
g_bytes_unref (bytes);
|
|
|
|
gtk_video_frame_ffmpeg_init (result,
|
|
texture,
|
|
av_rescale_q (frame->best_effort_timestamp,
|
|
self->input_video_stream->stream->time_base,
|
|
(AVRational) { 1, G_USEC_PER_SEC }));
|
|
|
|
av_frame_free (&frame);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static int64_t
|
|
gtk_ff_media_file_seek_cb (void *data,
|
|
int64_t offset,
|
|
int whence)
|
|
{
|
|
GtkFfMediaFile *self = data;
|
|
GSeekType seek_type;
|
|
gboolean result;
|
|
|
|
switch (whence)
|
|
{
|
|
case SEEK_SET:
|
|
seek_type = G_SEEK_SET;
|
|
break;
|
|
|
|
case SEEK_CUR:
|
|
seek_type = G_SEEK_CUR;
|
|
break;
|
|
|
|
case SEEK_END:
|
|
seek_type = G_SEEK_END;
|
|
break;
|
|
|
|
case AVSEEK_SIZE:
|
|
/* FIXME: Handle size querying */
|
|
return -1;
|
|
|
|
default:
|
|
g_assert_not_reached ();
|
|
return -1;
|
|
}
|
|
|
|
result = g_seekable_seek (G_SEEKABLE (self->input_stream),
|
|
offset,
|
|
seek_type,
|
|
NULL,
|
|
NULL);
|
|
if (!result)
|
|
return -1;
|
|
|
|
return g_seekable_tell (G_SEEKABLE (self->input_stream));
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_create_input_stream (GtkFfMediaFile *self)
|
|
{
|
|
GError *error = NULL;
|
|
GFile *file;
|
|
|
|
file = gtk_media_file_get_file (GTK_MEDIA_FILE (self));
|
|
if (file)
|
|
{
|
|
self->input_stream = G_INPUT_STREAM (g_file_read (file, NULL, &error));
|
|
if (self->input_stream == NULL)
|
|
{
|
|
gtk_media_stream_gerror (GTK_MEDIA_STREAM (self), error);
|
|
g_error_free (error);
|
|
return FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->input_stream = g_object_ref (gtk_media_file_get_input_stream (GTK_MEDIA_FILE (self)));
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static AVIOContext *
|
|
gtk_ff_media_file_create_io_context (GtkFfMediaFile *self)
|
|
{
|
|
AVIOContext *result;
|
|
int buffer_size = 4096; /* it's what everybody else uses... */
|
|
unsigned char *buffer;
|
|
|
|
if (!gtk_ff_media_file_create_input_stream (self))
|
|
return NULL;
|
|
|
|
buffer = av_malloc (buffer_size);
|
|
if (buffer == NULL)
|
|
return NULL;
|
|
|
|
result = avio_alloc_context (buffer,
|
|
buffer_size,
|
|
AVIO_FLAG_READ,
|
|
self,
|
|
gtk_ff_media_file_read_packet_cb,
|
|
NULL,
|
|
G_IS_SEEKABLE (self->input_stream)
|
|
? gtk_ff_media_file_seek_cb
|
|
: NULL);
|
|
|
|
result->buf_ptr = result->buf_end;
|
|
result->write_flag = 0;
|
|
|
|
return result;
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_init_audio_resampler (GtkFfMediaFile *self)
|
|
{
|
|
AVCodecContext *in_codec_ctx = self->input_audio_stream->codec_ctx;
|
|
AVCodecContext *out_codec_ctx = self->output_audio_stream->codec_ctx;
|
|
int errnum;
|
|
|
|
// create resampler context
|
|
self->swr_ctx = swr_alloc ();
|
|
if (!self->swr_ctx)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Could not allocate resampler context"));
|
|
return FALSE;
|
|
}
|
|
|
|
// set resampler option
|
|
av_opt_set_int (self->swr_ctx, "in_channel_count", in_codec_ctx->channels, 0);
|
|
av_opt_set_int (self->swr_ctx, "in_sample_rate", in_codec_ctx->sample_rate, 0);
|
|
av_opt_set_sample_fmt (self->swr_ctx, "in_sample_fmt", in_codec_ctx->sample_fmt, 0);
|
|
av_opt_set_int (self->swr_ctx, "out_channel_count", out_codec_ctx->channels, 0);
|
|
av_opt_set_int (self->swr_ctx, "out_sample_rate", out_codec_ctx->sample_rate, 0);
|
|
av_opt_set_sample_fmt (self->swr_ctx, "out_sample_fmt", out_codec_ctx->sample_fmt, 0);
|
|
|
|
// initialize the resampling context
|
|
errnum = swr_init (self->swr_ctx);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_open_audio_device (GtkFfMediaFile *self)
|
|
{
|
|
const AVOutputFormat *candidate;
|
|
int errnum;
|
|
|
|
/* Try finding an audio device that supports setting the volume */
|
|
for (candidate = av_output_audio_device_next (NULL);
|
|
candidate != NULL;
|
|
candidate = av_output_audio_device_next (candidate))
|
|
{
|
|
if (candidate->control_message)
|
|
break;
|
|
}
|
|
|
|
/* fallback to the first format available */
|
|
if (candidate == NULL)
|
|
candidate = av_output_audio_device_next (NULL);
|
|
|
|
if (candidate == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_ ("No audio output found"));
|
|
return FALSE;
|
|
}
|
|
|
|
errnum = avformat_alloc_output_context2 (&self->device_ctx, candidate, NULL, NULL);
|
|
if (errnum != 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean gtk_ff_media_file_play (GtkMediaStream *stream);
|
|
|
|
static void
|
|
gtk_ff_media_file_open (GtkMediaFile *file)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (file);
|
|
int errnum;
|
|
int nb_samples;
|
|
|
|
self->format_ctx = avformat_alloc_context ();
|
|
self->format_ctx->pb = gtk_ff_media_file_create_io_context (self);
|
|
if (self->format_ctx->pb == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_FAILED,
|
|
_("Not enough memory"));
|
|
return;
|
|
}
|
|
errnum = avformat_open_input (&self->format_ctx, NULL, NULL, NULL);
|
|
if (errnum != 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return;
|
|
}
|
|
|
|
errnum = avformat_find_stream_info (self->format_ctx, NULL);
|
|
if (errnum < 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return;
|
|
}
|
|
|
|
self->input_audio_stream = gtk_ff_media_file_find_input_stream (self, AVMEDIA_TYPE_AUDIO);
|
|
self->input_video_stream = gtk_ff_media_file_find_input_stream (self, AVMEDIA_TYPE_VIDEO);
|
|
|
|
// open an audio device when we have an audio stream
|
|
if (self->input_audio_stream && gtk_ff_media_file_open_audio_device (self))
|
|
{
|
|
self->output_audio_stream = gtk_ff_media_file_add_output_stream (self,
|
|
self->device_ctx,
|
|
self->device_ctx->oformat->audio_codec);
|
|
|
|
gtk_ff_media_file_init_audio_resampler (self);
|
|
|
|
if (self->output_audio_stream->codec_ctx->codec->capabilities & AV_CODEC_CAP_VARIABLE_FRAME_SIZE)
|
|
nb_samples = 10000; // just taken from the ffmpeg muxing example
|
|
else
|
|
nb_samples = self->output_audio_stream->codec_ctx->frame_size;
|
|
|
|
self->audio_frame = gtk_ff_media_file_alloc_audio_frame (self->output_audio_stream->codec_ctx->sample_fmt,
|
|
self->output_audio_stream->codec_ctx->channel_layout,
|
|
self->output_audio_stream->codec_ctx->sample_rate,
|
|
nb_samples);
|
|
|
|
if (self->audio_frame == NULL)
|
|
{
|
|
gtk_media_stream_error (GTK_MEDIA_STREAM (self),
|
|
G_IO_ERROR,
|
|
G_IO_ERROR_NOT_SUPPORTED,
|
|
_("Failed to allocate an audio frame"));
|
|
return;
|
|
}
|
|
|
|
errnum = avformat_write_header (self->device_ctx, NULL);
|
|
if (errnum != 0)
|
|
{
|
|
gtk_ff_media_file_set_ffmpeg_error (self, errnum);
|
|
return;
|
|
}
|
|
}
|
|
|
|
gtk_media_stream_stream_prepared (GTK_MEDIA_STREAM (self),
|
|
self->output_audio_stream != NULL,
|
|
self->input_video_stream != NULL,
|
|
TRUE,
|
|
self->format_ctx->duration != AV_NOPTS_VALUE
|
|
? av_rescale (self->format_ctx->duration, G_USEC_PER_SEC, AV_TIME_BASE)
|
|
: 0);
|
|
|
|
gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
|
|
|
|
if (gtk_ff_media_file_decode_frame (self, &self->current_frame))
|
|
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
|
|
|
|
if (gtk_media_stream_get_playing (GTK_MEDIA_STREAM (self)))
|
|
gtk_ff_media_file_play (GTK_MEDIA_STREAM (self));
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_close (GtkMediaFile *file)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (file);
|
|
|
|
g_clear_object (&self->input_stream);
|
|
|
|
g_clear_pointer (&self->swr_ctx, swr_close);
|
|
g_clear_pointer (&self->sws_ctx, sws_freeContext);
|
|
g_clear_pointer (&self->input_audio_stream, gtk_ff_stream_close);
|
|
g_clear_pointer (&self->input_video_stream, gtk_ff_stream_close);
|
|
g_clear_pointer (&self->output_audio_stream, gtk_ff_stream_close);
|
|
av_frame_free (&self->audio_frame);
|
|
avformat_free_context(self->device_ctx);
|
|
avformat_close_input (&self->format_ctx);
|
|
gtk_video_frame_ffmpeg_clear (&self->next_frame);
|
|
gtk_video_frame_ffmpeg_clear (&self->current_frame);
|
|
|
|
gdk_paintable_invalidate_size (GDK_PAINTABLE (self));
|
|
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_next_frame_cb (gpointer data);
|
|
static void
|
|
gtk_ff_media_file_queue_frame (GtkFfMediaFile *self)
|
|
{
|
|
gint64 time, frame_time;
|
|
guint delay;
|
|
|
|
time = g_get_monotonic_time ();
|
|
frame_time = self->start_time + self->next_frame.timestamp;
|
|
delay = time > frame_time ? 0 : (frame_time - time) / 1000;
|
|
|
|
self->next_frame_cb = g_timeout_add (delay, gtk_ff_media_file_next_frame_cb, self);
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_restart (GtkFfMediaFile *self)
|
|
{
|
|
if (!gtk_ff_media_file_seek_stream (self, self->input_audio_stream, 0))
|
|
return FALSE;
|
|
if (!gtk_ff_media_file_seek_stream (self, self->input_video_stream, 0))
|
|
return FALSE;
|
|
|
|
if (!gtk_ff_media_file_decode_frame (self, &self->next_frame))
|
|
return FALSE;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_next_frame_cb (gpointer data)
|
|
{
|
|
GtkFfMediaFile *self = data;
|
|
|
|
self->next_frame_cb = 0;
|
|
|
|
if (gtk_video_frame_ffmpeg_is_empty (&self->next_frame))
|
|
{
|
|
if (!gtk_media_stream_get_loop (GTK_MEDIA_STREAM (self)) ||
|
|
!gtk_ff_media_file_restart (self))
|
|
{
|
|
gtk_media_stream_stream_ended (GTK_MEDIA_STREAM (self));
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
self->start_time += self->current_frame.timestamp - self->next_frame.timestamp;
|
|
}
|
|
|
|
gtk_video_frame_ffmpeg_clear (&self->current_frame);
|
|
gtk_video_frame_ffmpeg_move (&self->current_frame,
|
|
&self->next_frame);
|
|
|
|
gtk_media_stream_update (GTK_MEDIA_STREAM (self),
|
|
self->current_frame.timestamp);
|
|
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
|
|
|
|
/* ignore failure here, we'll handle the empty frame case above
|
|
* the next time we're called. */
|
|
gtk_ff_media_file_decode_frame (self, &self->next_frame);
|
|
gtk_ff_media_file_queue_frame (self);
|
|
|
|
return G_SOURCE_REMOVE;
|
|
}
|
|
|
|
static gboolean
|
|
gtk_ff_media_file_play (GtkMediaStream *stream)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (stream);
|
|
|
|
if (self->format_ctx == NULL)
|
|
return FALSE;
|
|
|
|
if (!gtk_media_stream_is_prepared (stream))
|
|
return TRUE;
|
|
|
|
if (gtk_video_frame_ffmpeg_is_empty (&self->next_frame) &&
|
|
!gtk_ff_media_file_decode_frame (self, &self->next_frame))
|
|
{
|
|
if (gtk_ff_media_file_restart (self))
|
|
{
|
|
self->start_time = g_get_monotonic_time () - self->next_frame.timestamp;
|
|
}
|
|
else
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
self->start_time = g_get_monotonic_time () - self->current_frame.timestamp;
|
|
}
|
|
|
|
gtk_ff_media_file_queue_frame (self);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_pause (GtkMediaStream *stream)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (stream);
|
|
|
|
if (self->next_frame_cb)
|
|
{
|
|
g_source_remove (self->next_frame_cb);
|
|
self->next_frame_cb = 0;
|
|
}
|
|
|
|
self->start_time = 0;
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_seek (GtkMediaStream *stream,
|
|
gint64 timestamp)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (stream);
|
|
|
|
if (!gtk_ff_media_file_seek_stream (self, self->input_audio_stream, timestamp))
|
|
return;
|
|
if (!gtk_ff_media_file_seek_stream (self, self->input_video_stream, timestamp))
|
|
return;
|
|
|
|
gtk_media_stream_seek_success (stream);
|
|
|
|
gtk_video_frame_ffmpeg_clear (&self->next_frame);
|
|
gtk_video_frame_ffmpeg_clear (&self->current_frame);
|
|
if (gtk_ff_media_file_decode_frame (self, &self->current_frame))
|
|
gtk_media_stream_update (stream, self->current_frame.timestamp);
|
|
gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
|
|
|
|
if (gtk_media_stream_get_playing (stream))
|
|
{
|
|
gtk_ff_media_file_pause (stream);
|
|
if (!gtk_ff_media_file_play (stream))
|
|
gtk_media_stream_stream_ended (stream);
|
|
}
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_update_audio (GtkMediaStream *stream,
|
|
gboolean muted,
|
|
double volume)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (stream);
|
|
int errnum;
|
|
|
|
errnum = avdevice_app_to_dev_control_message (self->device_ctx, muted ? AV_APP_TO_DEV_MUTE : AV_APP_TO_DEV_UNMUTE, NULL, 0);
|
|
if (errnum < 0)
|
|
{
|
|
g_warning ("Cannot set audio mute state");
|
|
}
|
|
|
|
errnum = avdevice_app_to_dev_control_message (self->device_ctx, AV_APP_TO_DEV_SET_VOLUME, &volume, sizeof (volume));
|
|
if (errnum < 0)
|
|
{
|
|
g_warning ("Cannot set audio volume");
|
|
}
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_dispose (GObject *object)
|
|
{
|
|
GtkFfMediaFile *self = GTK_FF_MEDIA_FILE (object);
|
|
|
|
gtk_ff_media_file_pause (GTK_MEDIA_STREAM (self));
|
|
gtk_ff_media_file_close (GTK_MEDIA_FILE (self));
|
|
|
|
G_OBJECT_CLASS (gtk_ff_media_file_parent_class)->dispose (object);
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_class_init (GtkFfMediaFileClass *klass)
|
|
{
|
|
GtkMediaFileClass *file_class = GTK_MEDIA_FILE_CLASS (klass);
|
|
GtkMediaStreamClass *stream_class = GTK_MEDIA_STREAM_CLASS (klass);
|
|
GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
|
|
|
|
file_class->open = gtk_ff_media_file_open;
|
|
file_class->close = gtk_ff_media_file_close;
|
|
stream_class->play = gtk_ff_media_file_play;
|
|
stream_class->pause = gtk_ff_media_file_pause;
|
|
stream_class->seek = gtk_ff_media_file_seek;
|
|
stream_class->update_audio = gtk_ff_media_file_update_audio;
|
|
|
|
gobject_class->dispose = gtk_ff_media_file_dispose;
|
|
}
|
|
|
|
static void
|
|
gtk_ff_media_file_init (GtkFfMediaFile *self)
|
|
{
|
|
}
|
|
|
|
|