Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LU-3407 first frame #59

Merged
merged 4 commits into from
Aug 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lumen5/framefusion",
"version": "1.0.6",
"version": "1.0.7",
"type": "module",
"scripts": {
"docs": "typedoc framefusion.ts",
Expand Down
16 changes: 10 additions & 6 deletions src/backends/beamcoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ export class BeamcoderExtractor extends BaseExtractor implements Extractor {
}
this.#demuxer = await beamcoder.demuxer(inputFileOrUrl);
this.#streamIndex = this.#demuxer.streams.findIndex(stream => stream.codecpar.codec_type === STREAM_TYPE_VIDEO);

if (this.#streamIndex === -1) {
throw new Error(`File has no ${STREAM_TYPE_VIDEO} stream!`);
}
Expand Down Expand Up @@ -325,7 +326,7 @@ export class BeamcoderExtractor extends BaseExtractor implements Extractor {
* additional packets and find a frame that is closer to the targetPTS.
*/
async _getFrameAtPts(targetPTS: number, SeekPTSOffset = 0): Promise<beamcoder.Frame> {
VERBOSE && console.log('_getFrameAtPts', targetPTS, SeekPTSOffset, '-> duration', this.duration);
VERBOSE && console.log('_getFrameAtPts', targetPTS, 'seekPTSOffset', SeekPTSOffset, 'duration', this.duration);
this.#packetReadCount = 0;

// seek and create a decoder when retrieving a frame for the first time or when seeking backwards
Expand All @@ -338,9 +339,9 @@ export class BeamcoderExtractor extends BaseExtractor implements Extractor {
const hasFrameWithinThreshold = this.#filteredFramesPacket.flat().some(frame => {
return this.ptsToTime(Math.abs(targetPTS - (frame as Frame).pts)) < RE_SEEK_THRESHOLD;
});
VERBOSE && console.log('hasPreviousTargetPTS', this.#previousTargetPTS === null, 'targetPTS is smaller', this.#previousTargetPTS > targetPTS, 'has frame within threshold', hasFrameWithinThreshold);
VERBOSE && console.log('hasPreviousTargetPTS:', this.#previousTargetPTS === null, ', targetPTS is smaller:', this.#previousTargetPTS > targetPTS, ', has frame within threshold:', hasFrameWithinThreshold);
if (this.#previousTargetPTS === null || this.#previousTargetPTS > targetPTS || !hasFrameWithinThreshold) {
VERBOSE && console.log(`Seeking to ${targetPTS - SeekPTSOffset}`);
VERBOSE && console.log(`Seeking to ${targetPTS + SeekPTSOffset}`);

await this.#demuxer.seek({
stream_index: 0, // even though we specify the stream index, it still seeks all streams
Expand Down Expand Up @@ -390,19 +391,22 @@ export class BeamcoderExtractor extends BaseExtractor implements Extractor {
// Read packets until we have a frame which is closest to targetPTS
while ((this.#packet || this.#frames.length !== 0) && closestFramePTS < targetPTS) {
VERBOSE && console.log('packet si:', this.#packet?.stream_index, 'pts:', this.#packet?.pts, 'frames:', this.#frames?.length);
VERBOSE && console.log('frames', this.#frames?.length, 'frames.pts:', this.#frames?.map(f => f.pts), '-> target.pts:', targetPTS);
VERBOSE && console.log('frames', this.#frames?.length, 'frames.pts:', JSON.stringify(this.#frames?.map(f => f.pts)), '-> target.pts:', targetPTS);

// packet contains frames
if (this.#frames.length !== 0) {
// filter the frames
const filteredResult = await this.#filterer.filter([{ name: 'in0:v', frames: this.#frames }]);
filteredFrames = filteredResult.flatMap(r => r.frames);
VERBOSE && console.log('filteredFrames', filteredFrames.length, 'filteredFrames.pts:', filteredFrames.map(f => f.pts), '-> target.pts:', targetPTS);
VERBOSE && console.log('filteredFrames', filteredFrames.length, 'filteredFrames.pts:', JSON.stringify(filteredFrames.map(f => f.pts)), '-> target.pts:', targetPTS);

// get the closest frame to our target presentation timestamp (PTS)
// Beamcoder returns decoded packet frames as follows: [1000, 2000, 3000, 4000]
// If we're looking for a frame at 0, we want to return the frame at 1000
// If we're looking for a frame at 2500, we want to return the frame at 2000
const closestFrame = filteredFrames.reverse().find(f => f.pts <= targetPTS);
const closestFrame = (this.#packetReadCount === 1 && filteredFrames[0].pts > targetPTS)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will this work with looping too? like if we seek back to an earlier timestamp. I'm not sure how framefusion handles that but could be a case to consider? So if we play and then loop back to the start (maybe not a use case for AI avatar specifically now but is a use case more generally)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll add a test for that

Copy link
Contributor Author

@animanathome animanathome Aug 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we have a few tests for looping: can get frames at random times (forward and backward) and can get frames when looping. Both of them passed with my changes so I think we're good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but I'm guessing those tests don't use the new code path? Like the case for when PTS is greater than what we're looking for

? filteredFrames[0]
: filteredFrames.reverse().find(f => f.pts <= targetPTS);

// The packet contains frames, but all of them have PTS larger than our a targetPTS (we looked too far)
if (!closestFrame) {
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
22 changes: 21 additions & 1 deletion test/framefusion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,27 @@ describe('FrameFusion', () => {
await extractor.dispose();
});

it('can get first frame from vp9 encoded webm with alpha', async() => {
// Arrange
const extractor = await BeamcoderExtractor.create({
inputFileOrUrl: './test/samples/vp9-webm-with-alpha.webm',
threadCount: 8,
});

// Act and Assert
const imageData = await extractor.getImageDataAtTime(0);
const canvasImageData = createImageData(imageData.data, imageData.width, imageData.height);

const canvas = createCanvas(imageData.width, imageData.height);
const ctx = canvas.getContext('2d', { alpha: true });

ctx.putImageData(canvasImageData, 0, 0);
expect(canvas.toBuffer('image/png')).toMatchImageSnapshot();

// Cleanup
await extractor.dispose();
});

it('can get the same frame multiple times', async() => {
// When smaller increments are requested, the same frame can be returned multiple times. This happens when the
// caller plays the video at a lower playback rate than the source video.
Expand Down Expand Up @@ -418,7 +439,6 @@ describe('FrameFusion', () => {

it('should accurately generate frames when seeking to time that aligns with frame boundaries.', async() => {
// Arrange

// ffprobe -show_frames test/samples/count0To179.mp4 | grep pts
// pts=30720
// pts_time=2.000000
Expand Down
Loading