Seek through frames given a timestamp. (#9)

* Add input slide to Frames component for seeking frames.

* 0.4.0
This commit is contained in:
Alfred Gutierrez
2020-12-23 22:27:33 -08:00
committed by GitHub
parent b7d25bacfa
commit 9c1a3f5ff1
4 changed files with 1644 additions and 1366 deletions

View File

@@ -48,6 +48,7 @@ typedef struct Frame {
char pict_type; char pict_type;
int pts; int pts;
int dts; int dts;
int pos;
int pkt_size; int pkt_size;
} Frame; } Frame;
@@ -65,6 +66,8 @@ typedef struct FramesResponse {
std::vector<Frame> frames; std::vector<Frame> frames;
int nb_frames; int nb_frames;
int gop_size; int gop_size;
int duration;
double time_base;
} FramesResponse; } FramesResponse;
FileInfoResponse get_file_info(std::string filename) { FileInfoResponse get_file_info(std::string filename) {
@@ -138,7 +141,7 @@ FileInfoResponse get_file_info(std::string filename) {
return r; return r;
} }
FramesResponse get_frames(std::string filename, int offset) { FramesResponse get_frames(std::string filename, int timestamp) {
av_log_set_level(AV_LOG_QUIET); // No logging output for libav. av_log_set_level(AV_LOG_QUIET); // No logging output for libav.
FILE *file = fopen(filename.c_str(), "rb"); FILE *file = fopen(filename.c_str(), "rb");
@@ -191,8 +194,18 @@ FramesResponse get_frames(std::string filename, int offset) {
} }
} }
AVRational stream_time_base = pFormatContext->streams[video_stream_index]->time_base;
// printf("stream_time_base: %d / %d = %.5f\n", stream_time_base.num, stream_time_base.den, av_q2d(stream_time_base));
FramesResponse r; FramesResponse r;
r.nb_frames = nb_frames; r.nb_frames = nb_frames;
r.time_base = av_q2d(stream_time_base);
r.duration = pFormatContext->streams[video_stream_index]->duration;
// If the duration value isn't in the stream, get from the FormatContext.
if (r.duration == 0) {
r.duration = pFormatContext->duration * r.time_base;
}
AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec); AVCodecContext *pCodecContext = avcodec_alloc_context3(pCodec);
avcodec_parameters_to_context(pCodecContext, pCodecParameters); avcodec_parameters_to_context(pCodecContext, pCodecParameters);
@@ -204,13 +217,13 @@ FramesResponse get_frames(std::string filename, int offset) {
int max_packets_to_process = 1000; int max_packets_to_process = 1000;
int frame_count = 0; int frame_count = 0;
int key_frames = 0; int key_frames = 0;
int gop_size = 0;
// Seek to frame from the given timestamp.
av_seek_frame(pFormatContext, video_stream_index, timestamp, AVSEEK_FLAG_ANY);
// Read video frames. // Read video frames.
while (av_read_frame(pFormatContext, pPacket) >= 0) { while (av_read_frame(pFormatContext, pPacket) >= 0) {
if (pPacket->stream_index == video_stream_index) { if (pPacket->stream_index == video_stream_index) {
if (frame_count >= offset) {
int response = 0; int response = 0;
response = avcodec_send_packet(pCodecContext, pPacket); response = avcodec_send_packet(pCodecContext, pPacket);
@@ -231,23 +244,24 @@ FramesResponse get_frames(std::string filename, int offset) {
.pict_type = (char) av_get_picture_type_char(pFrame->pict_type), .pict_type = (char) av_get_picture_type_char(pFrame->pict_type),
.pts = (int) pPacket->pts, .pts = (int) pPacket->pts,
.dts = (int) pPacket->dts, .dts = (int) pPacket->dts,
.pos = (int) pPacket->pos,
.pkt_size = pFrame->pkt_size, .pkt_size = pFrame->pkt_size,
}; };
r.frames.push_back(f); r.frames.push_back(f);
if (--max_packets_to_process <= 0) break; if (--max_packets_to_process <= 0) break;
} }
}
frame_count++; frame_count++;
} }
av_packet_unref(pPacket); av_packet_unref(pPacket);
} }
r.gop_size = frame_count;
avformat_close_input(&pFormatContext); avformat_close_input(&pFormatContext);
av_packet_free(&pPacket); av_packet_free(&pPacket);
av_frame_free(&pFrame); av_frame_free(&pFrame);
avcodec_free_context(&pCodecContext); avcodec_free_context(&pCodecContext);
r.gop_size = frame_count - offset;
return r; return r;
} }
@@ -281,6 +295,7 @@ EMSCRIPTEN_BINDINGS(structs) {
.field("pict_type", &Frame::pict_type) .field("pict_type", &Frame::pict_type)
.field("pts", &Frame::pts) .field("pts", &Frame::pts)
.field("dts", &Frame::dts) .field("dts", &Frame::dts)
.field("pos", &Frame::pos)
.field("pkt_size", &Frame::pkt_size); .field("pkt_size", &Frame::pkt_size);
register_vector<Frame>("Frame"); register_vector<Frame>("Frame");
@@ -299,6 +314,8 @@ EMSCRIPTEN_BINDINGS(structs) {
.field("frames", &FramesResponse::frames) .field("frames", &FramesResponse::frames)
.field("nb_frames", &FramesResponse::nb_frames) .field("nb_frames", &FramesResponse::nb_frames)
.field("gop_size", &FramesResponse::gop_size) .field("gop_size", &FramesResponse::gop_size)
.field("duration", &FramesResponse::duration)
.field("time_base", &FramesResponse::time_base)
; ;
function("get_frames", &get_frames); function("get_frames", &get_frames);
} }

2888
www/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{ {
"name": "ffprobe-wasm", "name": "ffprobe-wasm",
"version": "0.3.0", "version": "0.4.0",
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
"build": "vue-cli-service build", "build": "vue-cli-service build",
@@ -23,8 +23,7 @@
"babel-eslint": "^10.1.0", "babel-eslint": "^10.1.0",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11"
"hello": "file:../dist"
}, },
"eslintConfig": { "eslintConfig": {
"root": true, "root": true,

View File

@@ -1,25 +1,24 @@
<template> <template>
<div> <div>
<h4>Frames</h4> <div v-if="!data" class="text-center mt-4">Loading...</div>
<div v-if="!data">Loading...</div>
<div v-if="data"> <div v-if="data">
<p class="float-left">GOP Size: {{ data.gop_size }}</p> <b-table
<p class="text-right">Total: {{ data.nb_frames }}</p> class="col-4"
:items="metadata"
thead-class="d-none"
small outlined
></b-table>
<b-pagination <div>
v-model="currentPage" <b-form-group label="Timestamp:" label-for="timestamp">
@change="onPageChanged" <b-input class="float-left col-2" v-model="value" @change="inputHandler"></b-input>
:total-rows="pages" <b-button class="float-right ml-2" @click="onNext">Next</b-button>
:per-page="perPage" <b-button class="float-right" @click="onPrev">Prev</b-button>
align="right" <b-input id="timestamp" v-model="value" type="range" min="0" :max="data.duration" @change="inputHandler"></b-input>
></b-pagination> </b-form-group>
</div>
<b-table striped hover :items="data.frames" :busy="isBusy"> <b-table striped hover :fields="fields" :items="data.frames" :busy="isBusy">
<template #table-busy>
<div class="text-center text-primary my-2">
<b-spinner class="align-middle"></b-spinner>
</div>
</template>
<template #cell(frame_number)="data"> <template #cell(frame_number)="data">
{{ data.value + 1 }} {{ data.value + 1 }}
</template> </template>
@@ -27,14 +26,6 @@
{{ String.fromCharCode(data.value) }} {{ String.fromCharCode(data.value) }}
</template> </template>
</b-table> </b-table>
<b-pagination
v-model="currentPage"
@change="onPageChanged"
:total-rows="pages"
:per-page="perPage"
align="right"
></b-pagination>
</div> </div>
</div> </div>
</template> </template>
@@ -45,33 +36,56 @@ export default {
props: ['file'], props: ['file'],
data() { data() {
return { return {
fields: [
{ key: 'frame_number', label: 'Index' },
'pict_type', 'pts', 'dts', 'pos', 'pkt_size'
],
data: null, data: null,
currentPage: 1,
isBusy: false, isBusy: false,
value: 0,
lastPts: 0,
ptsPageSize: 0,
}; };
}, },
computed: { computed: {
perPage() { metadata() {
return this.data.gop_size; return [
}, { name: 'GOP Size', value: this.data.gop_size },
pages() { { name: 'Duration', value: this.data.duration },
return this.data.nb_frames; { name: 'Timebase', value: this.data.time_base },
}, { name: 'Total Frames', value: this.data.nb_frames },
]
}
}, },
created() { created() {
this.$worker.onmessage = (e) => { this.$worker.onmessage = (e) => {
this.data = e.data; this.data = e.data;
this.isBusy = false; this.isBusy = false;
this.value = e.data.frames.length > 0 ? e.data.frames[0].pts : 0;
} }
this.$worker.postMessage([ 'get_frames', this.file, 0 ]); this.$worker.postMessage([ 'get_frames', this.file, 0 ]);
}, },
methods: { methods: {
onPageChanged(page) { inputHandler(value) {
this.isBusy = true; this.getFrames(value);
this.$worker.postMessage([ 'get_frames', this.file, this.perPage * (page - 1) ]);
window.scrollTo(0, 0);
}, },
onNext() {
this.lastPts = this.data.frames[0].pts;
const nextPts = this.data.frames[this.data.frames.length - 1].pts;
this.getFrames(nextPts);
},
onPrev() {
if (this.ptsPageSize === 0) {
this.ptsPageSize = this.data.frames[0].pts - this.lastPts;
}
const prevPts = this.data.frames[0].pts - this.ptsPageSize;
this.getFrames(prevPts);
},
getFrames(value) {
this.isBusy = true;
this.$worker.postMessage([ 'get_frames', this.file, parseInt(value) ]);
window.scrollTo(0, 0);
}
} }
} }
</script> </script>