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

Using with nodejs fs #25

Closed
KaliaJS opened this issue Aug 21, 2023 · 12 comments
Closed

Using with nodejs fs #25

KaliaJS opened this issue Aug 21, 2023 · 12 comments
Labels
bug Something isn't working help wanted Extra attention is needed

Comments

@KaliaJS
Copy link

KaliaJS commented Aug 21, 2023

Hi Vanilagy,

For an electronjs app, I have to stream the creation of a video without being able to use the Web File System API.

So I use "fs" and I wanted to know if there is a possibility to stream like the Web File System API? Currently I'm using the buffer but it's not ideal because I have long 4K videos.

Do you have the possibility to do something?

Thank you !

@KaliaJS KaliaJS changed the title Using with fs.createWriteStream Using with nodejs fs Aug 21, 2023
@Vanilagy
Copy link
Owner

Hey!

This is totally possible by using the StreamTarget instead of the ArrayBufferTarget, as specified in the README. Here's a way you could do it with Node:

const fs = require('fs');
import { Muxer, StreamTarget } from 'webm-muxer';

const fileStream = fs.createWriteStream('output.webm', { flags: 'r+' });

let muxer = new Muxer({
    target: new StreamTarget(
        (data, position) => {
            fileStream.pos = position;
            fileStream.write(data);
        },
        () => {
            fileStream.end();
        }
    ),
    // ...
});

Let me know if this works for you.

@Vanilagy Vanilagy added the help wanted Extra attention is needed label Aug 21, 2023
@KaliaJS
Copy link
Author

KaliaJS commented Aug 22, 2023

Thank you for your help! I just pay you a ko-fi.

It works well but there is just one thing that doesn't work anymore, it's the seeking.

Normally with firstTimestampBehavior set to 'offset'` it should work normally right? I should be able to have a seekable player bar ?

    const [ track ] = stream.value.getTracks()
    const trackSettings = track.getSettings()
    processor = new MediaStreamTrackProcessor(track)
    inputStream = processor.readable

    worker.postMessage({
      command: 'init',
      ...
      stream: inputStream
    }, [ inputStream ])

worker

fileStream = createWriteStream(join(DIRECTORY_PATH, `${newFilename}_${nanoid()}.webm`))

  muxer = new Muxer({
    target: new StreamTarget(
      (data, position) => {
        fileStream.pos = position
        fileStream.write(data)
      },
      () => fileStream.end()
    ),
    video: {...},
    audio: false,
    firstTimestampBehavior: 'offset'
  })

  encoder = new VideoEncoder({
    output: (chunk, meta) => muxer.addVideoChunk(chunk, meta),
    error: ({message}) => postMessage({ command: 'error', message })
  })

  ...

  frameReader = stream.getReader()

  if (!existsSync(DIRECTORY_PATH)) {
    mkdirSync(DIRECTORY_PATH, { recursive: true })
  }
  
  let frameCounter = 0

  frameReader.read().then(async function processFrame({ done, value: frame }) {
    if (done) {
      postMessage({ command: 'finishing' })
      await encoder.flush()
      await muxer.finalize()
      encoder.close()
      return postMessage({ command: 'completed' })
    }

    if (encoder.encodeQueueSize <= config.framerate) {
      ++frameCounter % 20
      const insert_keyframe = (frameCounter % 150) == 0
      encoder.encode(frame, { keyFrame: insert_keyframe })
    }

    frame.close()
    frameReader.read().then(processFrame)
  })

@KaliaJS
Copy link
Author

KaliaJS commented Aug 22, 2023

I also have two good practice questions.

  1. Is it better to move everything into the end callback of the muxer?
new Muxer({
    target: new StreamTarget(
      (data, position) => {
        fileStream.pos = position
        fileStream.write(data)
      },
      async () => {
        // put here ?
        postMessage({ command: 'finishing' })
        await encoder.flush()
        muxer.finalize()
        fileStream.end()
        postMessage({ command: 'completed' })
      }
    ),
    ...
  })

or it better here ?

  frameReader.read().then(async function processFrame({ done, value: frame }) {
   if (done) {
      // or here ?
      postMessage({ command: 'finishing' })
      await encoder.flush()
      muxer.finalize()
      return postMessage({ command: 'completed' })
    }
    ...
  })
  1. I saw that you never close the VideoEncoder in the demos with the close() method. Is there a reason for that?

@Vanilagy
Copy link
Owner

Vanilagy commented Aug 30, 2023

Thank you for the Ko-fi!! <3 🐡

To your best practice questions: Moving everything into the end callback of the muxer doesn't make sense, as it will only be called when you call finalize. Honestly, there isn't much you need to do in that onDone callback - you could do it all right after you call finalize - same effect.

Now regarding the seeking: Strange; typically, in my experience, dysfunctional seeking means the file was written incorrectly. If you encode the file as you are right now, but use the ArrayBufferTarget instead, does the seeking remain broken? If it works, then there's an issue with the StreamTarget. Either a bug on my side or an error in your usage which I have yet to spot.

Would be awesome if you could send me some of the incorrect files, and also try out the ArrayBufferTarget thing and see if that fixes seeking :)

@Vanilagy
Copy link
Owner

Vanilagy commented Sep 8, 2023

Still need help?

@KaliaJS
Copy link
Author

KaliaJS commented Sep 10, 2023

@Vanilagy Sorry for the late response.

Thank you for your work

  1. OK I see clearly. I'll put it after finalize.
  2. Everything works fine with the ArrayBufferTarget.

I'll take care of making a repository for you and sending you an example of a broken file. Give me a few days.

@nsharma1396
Copy link

I am getting the same issue where saved files is not seekable after saving via the StreamTarget approach. At times, file was having incorrect seeking info, for example, it produced duration of 10 seconds for a 20 seconds file.
Files are seekable with the ArrayBufferTarget

@Vanilagy

@Vanilagy
Copy link
Owner

Understood, it seems like there might be a bug with StreamTarget. Alternatively, it could be that we're using Node's API wrong, but I don't see how.

Let's try to test StreamTarget by using its output to construct a single ArrayBuffer, which we'll then write to disk. If StreamTarget works fine, this should be identical to using ArrayBufferTarget - if not, we'll know the culprit.

Can you try using this sort of setup to test this?

import { Muxer, StreamTarget } from 'webm-muxer';

let chunkArray: { data: Uint8Array; position: number }[] = [];
let totalLength = 0;

const onData = (data: Uint8Array, position: number) => {
    chunkArray.push({ data, position });
    totalLength = Math.max(totalLength, position + data.length);
};

const onDone = () => {
    let finalBuffer = new ArrayBuffer(totalLength);
    let finalUint8 = new Uint8Array(finalBuffer);

    for (const { data, position } of chunkArray) {
        finalUint8.set(data, position);
    }

    fs.writeFile('output.webm', finalUint8);
};

let muxer = new Muxer({
    target: new StreamTarget(onData, onDone),
    // ...
});

Check if output.webm contains the same issue.

@Vanilagy Vanilagy added the bug Something isn't working label Sep 19, 2023
@Vanilagy
Copy link
Owner

Vanilagy commented Sep 19, 2023

Oh, I think we're using Node's streams wrong, don't think they support switching position. Let's try this again:

import { open } from 'fs/promises'; 
import { Muxer, StreamTarget } from 'webm-muxer';

let fileHandle = await open('output.webm', 'w+');

let muxer = new Muxer({
    target: new StreamTarget(
        (data, position) => {
            fileHandle.write(data, 0, data.length, position);
        },
        () => {
            fileHandle.close();
        },
        { chunked: true } // Writes larger chunks at once for better performance 
    ),
    // ...
});

Can you try this?

@nsharma1396
Copy link

Sorry for the late response
I tried this out and it worked!! Thanks so much once again!

Although, just for reference for others:
We cannot call fileHandle.close in the onDone callback as the flushed data buffer from webm-muxer will still not be written to the file yet.

Also as per nodejs doc:

It is unsafe to use filehandle.write() multiple times on the same file without waiting for the promise to be resolved (or rejected).

So, I had to implement this in a way that waits for the write to be completed before writing the next data chunk and calls fileHandle.close once the following two conditions are met:

  1. All data chunks are flushed (onDone callback)
  2. All data chunks are written

I may be wrong in any of my above assumptions due to lesser familiarity with fileHandles, but I did this to make things work properly.

@Vanilagy
Copy link
Owner

Awesome, and good that you caught this detail. For completion, here's one way to implement this using a Promise chain:

import { open } from 'fs/promises';
import { Muxer, StreamTarget } from 'webm-muxer';

let lastPromise = Promise.resolve();
let fileHandle = await open('output.webm', 'w+');

let muxer = new Muxer({
    target: new StreamTarget(
        (data, position) => {
            lastPromise = lastPromise.then(() => 
                fileHandle.write(data, 0, data.length, position)
            );
        },
        () => {
            lastPromise = lastPromise.then(() => 
                fileHandle.close()
            );
        },
        { chunked: true }  // Writes larger chunks at once for better performance
    ),
    // ...
});

@KaliaJS
Copy link
Author

KaliaJS commented Sep 24, 2023

Sorry, I haven't been very available in recent weeks. Thanks for finding the problem to you @Vanilagy and @nsharma1396

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants