566 lines
15 KiB
C
566 lines
15 KiB
C
/* -*- tab-width:2;c-file-style:"cc-mode"; -*- */
|
|
/*
|
|
* oxframe.c -- dump frame from an Ogg or WebM file
|
|
* Copyright (C) 2010 <j@mailb.org>
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* 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 General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with This program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
#if !defined(_GNU_SOURCE)
|
|
#define _GNU_SOURCE
|
|
#endif
|
|
#if !defined(_LARGEFILE_SOURCE)
|
|
#define _LARGEFILE_SOURCE
|
|
#endif
|
|
#if !defined(_LARGEFILE64_SOURCE)
|
|
#define _LARGEFILE64_SOURCE
|
|
#endif
|
|
#if !defined(_FILE_OFFSET_BITS)
|
|
#define _FILE_OFFSET_BITS 64
|
|
#endif
|
|
|
|
#include <assert.h>
|
|
#include <stdarg.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <getopt.h>
|
|
#include <oggplay/oggplay.h>
|
|
#include <nestegg.h>
|
|
|
|
#define VPX_CODEC_DISABLE_COMPAT 1
|
|
#include "vpx/vpx_decoder.h"
|
|
#include "vpx/vp8dx.h"
|
|
|
|
#define interface (&vpx_codec_vp8_dx_algo)
|
|
|
|
#include <string.h>
|
|
|
|
#include <X11/Xlib.h>
|
|
#include <Imlib2.h>
|
|
|
|
|
|
typedef enum {
|
|
oxImageNotSet,
|
|
oxPNG,
|
|
oxJPG,
|
|
} oxImageFormat;
|
|
|
|
typedef struct _oxstate {
|
|
const char *input;
|
|
const char *output;
|
|
oxImageFormat format;
|
|
long frame_pos;
|
|
long duration;
|
|
int image_width;
|
|
int image_height;
|
|
} oxstate;
|
|
|
|
static void usage(void){
|
|
fprintf(stderr,
|
|
"Usage: oxframe [options]\n\n"
|
|
"Options: \n\n"
|
|
" -h, --help show this help message and exit\n"
|
|
" -x WIDTH, --width=WIDTH\n"
|
|
" scale image to given width\n"
|
|
" -y HEIGHT, --height=HEIGHT\n"
|
|
" scale image to given height\n"
|
|
" -p POS, --pos=POS frame position in seconds, float\n"
|
|
" -i INPUT, --input=INPUT\n"
|
|
" video input\n"
|
|
" -o OUTPUT, --output=OUTPUT\n"
|
|
" path to save frame to, jpg, png supported\n"
|
|
" (defaults to png)\n"
|
|
" -f FORMAT, --output=FORMAT\n"
|
|
" output format, jpg or png\n"
|
|
" if not provided detected from output extension\n"
|
|
);
|
|
exit(0);
|
|
}
|
|
|
|
void write_image_file(OggPlayRGBChannels *data, oxstate * state) {
|
|
|
|
Imlib_Image *frame = NULL,
|
|
*image = NULL;
|
|
|
|
frame = imlib_create_image_using_data(data->rgb_width, data->rgb_height,
|
|
(unsigned int *)data->ptro);
|
|
|
|
if (state->image_width > 0 && state->image_height < 0) {
|
|
state->image_height = data->rgb_height * state->image_width / data->rgb_width;
|
|
}
|
|
if (state->image_height > 0 && state->image_width < 0) {
|
|
state->image_width = data->rgb_width * state->image_height / data->rgb_height;
|
|
}
|
|
if (state->image_width > 0) {
|
|
image = imlib_create_image(state->image_width, state->image_height);
|
|
imlib_context_set_image(image);
|
|
imlib_blend_image_onto_image(frame, 0,
|
|
0, 0, data->rgb_width, data->rgb_height,
|
|
0, 0,
|
|
state->image_width, state->image_height);
|
|
} else {
|
|
imlib_context_set_image(frame);
|
|
}
|
|
|
|
if(state->format == oxPNG)
|
|
imlib_image_set_format("png");
|
|
else
|
|
imlib_image_set_format("jpg");
|
|
imlib_save_image(state->output);
|
|
imlib_free_image_and_decache();
|
|
|
|
imlib_context_set_image(frame);
|
|
imlib_free_image_and_decache();
|
|
if (state->image_width > 0) {
|
|
imlib_context_set_image(image);
|
|
imlib_free_image_and_decache();
|
|
}
|
|
}
|
|
|
|
void write_frame (OggPlay * player, int track_num,
|
|
OggPlayVideoData * video_data, oxstate * state) {
|
|
|
|
OggPlayYUVChannels from;
|
|
OggPlayRGBChannels to;
|
|
|
|
from.ptry = video_data->y;
|
|
from.ptru = video_data->u;
|
|
from.ptrv = video_data->v;
|
|
oggplay_get_video_y_size(player, track_num, &(from.y_width),
|
|
&(from.y_height));
|
|
oggplay_get_video_uv_size(player, track_num, &(from.uv_width),
|
|
&(from.uv_height));
|
|
|
|
/*
|
|
printf("size: %dx%d %dx%d\n", from.y_width, from.y_height, from.uv_width,
|
|
from.uv_height);
|
|
*/
|
|
to.ptro = malloc(from.y_width * from.y_height * 4);
|
|
to.rgb_width = from.y_width;
|
|
to.rgb_height = from.y_height;
|
|
|
|
oggplay_yuv2bgra (&from, &to);
|
|
|
|
write_image_file(&to, state);
|
|
free(to.ptro);
|
|
}
|
|
|
|
int dump_frame_callback (OggPlay *player, int num_tracks,
|
|
OggPlayCallbackInfo **track_info, void *user) {
|
|
|
|
int i;
|
|
OggPlayDataHeader ** headers;
|
|
OggPlayVideoData * video_data;
|
|
OggPlayDataType type;
|
|
oxstate * state = (oxstate *)user;
|
|
|
|
for (i = 0; i < num_tracks; i++) {
|
|
type = oggplay_callback_info_get_type(track_info[i]);
|
|
headers = oggplay_callback_info_get_headers(track_info[i]);
|
|
|
|
switch (type) {
|
|
case OGGPLAY_INACTIVE:
|
|
break;
|
|
case OGGPLAY_YUV_VIDEO:
|
|
if (oggplay_callback_info_get_required(track_info[i]) < 1) {
|
|
//fprintf(stderr, "oops\n");
|
|
break;
|
|
}
|
|
long pt = oggplay_callback_info_get_presentation_time(headers[0]);
|
|
//printf("time: %ld %ld %ld\n", pt, state->duration, state->duration-pt);
|
|
if (pt >= state->frame_pos) {
|
|
video_data = oggplay_callback_info_get_video_data(headers[0]);
|
|
write_frame(player, i, video_data, state);
|
|
exit(0);
|
|
} else if (state->duration - pt < 500) {
|
|
video_data = oggplay_callback_info_get_video_data(headers[0]);
|
|
write_frame(player, i, video_data, state);
|
|
}
|
|
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void init_state(oxstate *state) {
|
|
state->input = NULL;
|
|
state->output = NULL;
|
|
state->frame_pos = 0;
|
|
state->duration = 0;
|
|
state->image_width = -1;
|
|
state->image_height = -1;
|
|
state->format = oxImageNotSet;
|
|
}
|
|
|
|
|
|
//WebM
|
|
static int
|
|
stdio_read(void * p, size_t length, void * fp)
|
|
{
|
|
size_t r;
|
|
|
|
r = fread(p, length, 1, fp);
|
|
if (r == 0 && feof(fp)) {
|
|
return 0;
|
|
}
|
|
return r == 0 ? -1 : 1;
|
|
}
|
|
|
|
static int
|
|
stdio_seek(int64_t offset, int whence, void * fp)
|
|
{
|
|
return fseek(fp, offset, whence);
|
|
}
|
|
|
|
static int64_t
|
|
stdio_tell(void * fp)
|
|
{
|
|
return ftell(fp);
|
|
}
|
|
|
|
static void
|
|
log_callback(nestegg * ctx, unsigned int severity, char const * fmt, ...)
|
|
{
|
|
va_list ap;
|
|
char const * sev = NULL;
|
|
|
|
#ifndef DEBUG
|
|
if (severity < NESTEGG_LOG_WARNING)
|
|
return;
|
|
#endif
|
|
|
|
switch (severity) {
|
|
case NESTEGG_LOG_DEBUG:
|
|
sev = "debug: ";
|
|
break;
|
|
case NESTEGG_LOG_WARNING:
|
|
sev = "warning: ";
|
|
break;
|
|
case NESTEGG_LOG_CRITICAL:
|
|
sev = "critical:";
|
|
break;
|
|
default:
|
|
sev = "unknown: ";
|
|
}
|
|
|
|
fprintf(stderr, "%p %s ", (void *) ctx, sev);
|
|
|
|
va_start(ap, fmt);
|
|
vfprintf(stderr, fmt, ap);
|
|
va_end(ap);
|
|
|
|
fprintf(stderr, "\n");
|
|
}
|
|
static void die_codec(vpx_codec_ctx_t *ctx, const char *s) {
|
|
const char *detail = vpx_codec_error_detail(ctx);
|
|
|
|
printf("%s: %s\n", s, vpx_codec_error(ctx));
|
|
if(detail)
|
|
printf(" %s\n",detail);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
int extract_frame_ogv(oxstate *state) {
|
|
int i;
|
|
int fps_num = 25;
|
|
int fps_denom = 1;
|
|
int granuleshift = 6;
|
|
int has_video = 0;
|
|
long offset;
|
|
|
|
OggPlay * player;
|
|
OggPlayReader * reader = NULL;
|
|
|
|
reader = oggplay_file_reader_new(state->input);
|
|
player = oggplay_open_with_reader(reader);
|
|
|
|
if (player == NULL) {
|
|
fprintf (stderr, "could not initialise oggplay with this file\n");
|
|
exit (1);
|
|
}
|
|
|
|
for (i = 0; i < oggplay_get_num_tracks (player); i++) {
|
|
if (oggplay_get_track_type (player, i) == OGGZ_CONTENT_THEORA) {
|
|
oggplay_set_callback_num_frames (player, i, 1);
|
|
oggplay_get_video_fps(player, i, &fps_denom, &fps_num);
|
|
has_video=1;
|
|
}
|
|
oggplay_set_track_active(player, i);
|
|
}
|
|
if(!has_video) {
|
|
fprintf (stderr, "no video track found\n");
|
|
exit (1);
|
|
}
|
|
oggplay_set_data_callback(player, dump_frame_callback, state);
|
|
|
|
offset = 500 + 1000 * ((1 << granuleshift) - 1) * fps_denom / fps_num;
|
|
//fprintf (stderr, "granule: %d offset %ld\n", granuleshift, offset);
|
|
|
|
state->duration = oggplay_get_duration(player);
|
|
|
|
/*
|
|
if (frame_pos > state->duration) {
|
|
fprintf (stderr, "can not seek to frame later than duration\n");
|
|
exit (1);
|
|
}
|
|
*/
|
|
|
|
if(state->frame_pos - offset > 0) {
|
|
if (oggplay_seek(player, state->frame_pos - offset) == E_OGGPLAY_CANT_SEEK) {
|
|
fprintf (stderr, "failed to seeek to %ld\n", state->frame_pos);
|
|
exit (1);
|
|
}
|
|
}
|
|
oggplay_start_decoding(player);
|
|
|
|
oggplay_close (player);
|
|
return 0;
|
|
}
|
|
|
|
int extract_frame_webm(oxstate *state) {
|
|
FILE * fp;
|
|
int r, type, codec_id;
|
|
nestegg * ctx;
|
|
nestegg_packet * pkt;
|
|
nestegg_video_params vparams;
|
|
uint64_t duration, pkt_tstamp;
|
|
unsigned int i, tracks;
|
|
int flags = 0;
|
|
int done = 0;
|
|
vpx_codec_ctx_t codec;
|
|
vpx_image_t *img;
|
|
|
|
//in nanoseconds
|
|
uint64_t seek_tstamp = (uint64_t)state->frame_pos*1000000;
|
|
|
|
nestegg_io io = {
|
|
stdio_read,
|
|
stdio_seek,
|
|
stdio_tell,
|
|
NULL
|
|
};
|
|
|
|
fp = fopen(state->input, "rb");
|
|
if (!fp) {
|
|
fprintf (stderr, "could not open input file\n");
|
|
exit (1);
|
|
}
|
|
io.userdata = fp;
|
|
ctx = NULL;
|
|
r = nestegg_init(&ctx, io, log_callback);
|
|
if (r != 0)
|
|
return EXIT_FAILURE;
|
|
|
|
nestegg_track_count(ctx, &tracks);
|
|
nestegg_duration(ctx, &duration);
|
|
|
|
if (seek_tstamp > duration) {
|
|
fprintf (stderr, "can not seek to frame later than duration\n");
|
|
exit (1);
|
|
}
|
|
|
|
/* Initialize codec */
|
|
if(vpx_codec_dec_init(&codec, interface, NULL, flags))
|
|
die_codec(&codec, "Failed to initialize decoder");
|
|
|
|
for (i = 0; i < tracks; ++i) {
|
|
type = nestegg_track_type(ctx, i);
|
|
codec_id = nestegg_track_codec_id(ctx, i);
|
|
|
|
if (type == NESTEGG_TRACK_VIDEO && codec_id == NESTEGG_CODEC_VP8) {
|
|
nestegg_track_video_params(ctx, i, &vparams);
|
|
|
|
if(!nestegg_track_seek(ctx, i, seek_tstamp)) {
|
|
|
|
while (!done && (r = nestegg_read_packet(ctx, &pkt)) > 0) {
|
|
unsigned int track;
|
|
|
|
nestegg_packet_track(pkt, &track);
|
|
if(nestegg_packet_tstamp(pkt, &pkt_tstamp) < 0) {
|
|
fprintf (stderr, "faild to get timestamp\n");
|
|
}
|
|
|
|
// only look for video track
|
|
if (track == i) {
|
|
unsigned int chunk, chunks;
|
|
|
|
nestegg_packet_count(pkt, &chunks);
|
|
|
|
// Decode each chunk of data.
|
|
for (chunk = 0; chunk < chunks; ++chunk) {
|
|
vpx_codec_iter_t iter = NULL;
|
|
unsigned char * data;
|
|
size_t data_size;
|
|
|
|
nestegg_packet_data(pkt, chunk, &data, &data_size);
|
|
|
|
/* Decode the frame */
|
|
if(vpx_codec_decode(&codec, data, data_size, NULL, 0))
|
|
die_codec(&codec, "Failed to decode frame");
|
|
|
|
while((img = vpx_codec_get_frame(&codec, &iter))) {
|
|
//we got a frame...
|
|
|
|
if(seek_tstamp <= pkt_tstamp) {
|
|
unsigned int y;
|
|
unsigned char *q, *p, *q2, *p2;
|
|
OggPlayYUVChannels from;
|
|
OggPlayRGBChannels to;
|
|
|
|
done = 1;
|
|
|
|
from.y_width = img->d_w;
|
|
from.y_height = img->d_h;
|
|
from.uv_width = (1 + img->d_w) / 2;
|
|
from.uv_height = (1 + img->d_h) / 2;
|
|
|
|
from.ptry = malloc(from.y_width * from.y_height);
|
|
from.ptru = malloc(from.uv_width * from.uv_height);
|
|
from.ptrv = malloc(from.uv_width * from.uv_height);
|
|
|
|
q =img->planes[VPX_PLANE_Y];
|
|
p = from.ptry;
|
|
for(y=0; y<img->d_h; y++) {
|
|
memcpy(p, q, img->d_w);
|
|
p += img->d_w;
|
|
q += img->stride[VPX_PLANE_Y];
|
|
}
|
|
q =img->planes[VPX_PLANE_U];
|
|
p = from.ptru;
|
|
q2 =img->planes[VPX_PLANE_V];
|
|
p2 = from.ptrv;
|
|
for(y=0; y<(1 + img->d_h) / 2; y++) {
|
|
memcpy(p, q, (1 + img->d_w) / 2);
|
|
memcpy(p2, q2, (1 + img->d_w) / 2);
|
|
p += (1 + img->d_w) / 2;
|
|
q += img->stride[VPX_PLANE_U];
|
|
p2 += (1 + img->d_w) / 2;
|
|
q2 += img->stride[VPX_PLANE_V];
|
|
}
|
|
|
|
to.ptro = malloc(from.y_width * from.y_height * 4);
|
|
to.rgb_width = from.y_width;
|
|
to.rgb_height = from.y_height;
|
|
|
|
oggplay_yuv2bgra (&from, &to);
|
|
|
|
write_image_file(&to, state);
|
|
free(from.ptry);
|
|
free(from.ptru);
|
|
free(from.ptrv);
|
|
free(to.ptro);
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
nestegg_free_packet(pkt);
|
|
}
|
|
|
|
if(vpx_codec_destroy(&codec))
|
|
die_codec(&codec, "Failed to destroy codec");
|
|
|
|
nestegg_destroy(ctx);
|
|
|
|
} else {
|
|
fprintf (stderr, "failed to seek\n");
|
|
exit (1);
|
|
}
|
|
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
int main (int argc, char * argv[]) {
|
|
|
|
int c,long_option_index;
|
|
oxstate state;
|
|
|
|
const char *optstring = "h:x:y:p:i:o:f:";
|
|
struct option options [] = {
|
|
{"help",required_argument,NULL,'h'},
|
|
{"width",required_argument,NULL,'x'},
|
|
{"height",required_argument,NULL,'y'},
|
|
{"pos",required_argument,NULL,'p'},
|
|
{"input",required_argument,NULL,'i'},
|
|
{"output",required_argument,NULL,'o'},
|
|
{"format",required_argument,NULL,'f'},
|
|
{NULL,0,NULL,0}
|
|
|
|
};
|
|
|
|
init_state(&state);
|
|
|
|
while(1) {
|
|
c=getopt_long(argc, argv, optstring, options, &long_option_index);
|
|
if(c == EOF)
|
|
break;
|
|
|
|
switch(c) {
|
|
case 'h':
|
|
usage();
|
|
break;
|
|
case 'x':
|
|
state.image_width = atoi(optarg);
|
|
break;
|
|
case 'y':
|
|
state.image_height = atoi(optarg);
|
|
break;
|
|
case 'p':
|
|
state.frame_pos = 1000 * atof(optarg);
|
|
break;
|
|
case 'i':
|
|
state.input = optarg;
|
|
break;
|
|
case 'o':
|
|
state.output = optarg;
|
|
case 'f':
|
|
if (strstr(optarg, "jpg") == NULL)
|
|
state.format = oxPNG;
|
|
else
|
|
state.format = oxJPG;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if(argc < 3) {
|
|
usage();
|
|
}
|
|
|
|
if(state.format == oxImageNotSet) {
|
|
if (strstr(&(state.output[strlen(state.output)-4]), ".jpg") == NULL)
|
|
state.format = oxPNG;
|
|
else
|
|
state.format = oxJPG;
|
|
}
|
|
|
|
if (state.input == NULL) {
|
|
fprintf (stderr, "please provide input file\n");
|
|
exit (1);
|
|
}
|
|
|
|
if (strstr(&(state.input[strlen(state.input)-5]), ".webm") == NULL) { //ogv
|
|
extract_frame_ogv(&state);
|
|
}
|
|
else { // .webm using nestegg + libvpx
|
|
extract_frame_webm(&state);
|
|
}
|
|
|
|
return 0;
|
|
}
|