tkVideoPlayer icon indicating copy to clipboard operation
tkVideoPlayer copied to clipboard

Can we seek the video based on video frame?

Open Akascape opened this issue 2 years ago • 20 comments

tkVideoPlayer only seeks in seconds, but I want to seek the video based on frame number of video, can we do this with tkVideoPlayer and also the progress bar based on the number of frames?

Akascape avatar May 31 '22 06:05 Akascape

Hello @Akascape, You can surely make a progress bar based on frames, we have <<FrameGenerated>> virtual event that is generated whenever a new frame is read from the file. The ability to seek to specific frame was removed from version 2.0.0 . I would strongly recommend not to downgrade from 2.0.0 as there were memory problems with prior versions of 2.0.0. If you can wait till tomorrow I can come up with something to seek specific frames, as I am busy with some work now. Thank you.

PaulleDemon avatar May 31 '22 06:05 PaulleDemon

@Akascape In the meantime can you check this algorithm?

sec = (1/frame_rate) * number_of _frames_to_skip

You can get the frame_rate from the video_info() method you can then pass the sec to seek() method.

Let me know if it works

PaulleDemon avatar May 31 '22 06:05 PaulleDemon

ok thanks, I would like to give some more information on a project I am currently working, Actually I want to implement a video player in a program. You can see how it looks: Akascape Screenshot

Akascape avatar May 31 '22 06:05 Akascape

What I want is the right window to show the video frame and the slider to seek the video frame

Akascape avatar May 31 '22 06:05 Akascape

@Akascape Your project looks Cool! Also, Did you check out this example? https://github.com/PaulleDemon/tkVideoPlayer/blob/master/examples/sample_player.py , though it does not seek specific frames, it would give you an idea of how I implemented something similar.

PaulleDemon avatar May 31 '22 06:05 PaulleDemon

Yes, I took the reference from there only.

Akascape avatar May 31 '22 06:05 Akascape

@Akascape In the meantime can you check this algorithm?

sec = (1/frame_rate) * number_of _frames_to_skip

You can get the frame_rate from the video_info() method you can then pass the sec to seek() method.

Let me know if it works

Alright Try this then and let me know.

PaulleDemon avatar May 31 '22 06:05 PaulleDemon

BTW what about the sound? This doesn't matter much in my project but still wanted to know if you are working on it.

Akascape avatar May 31 '22 07:05 Akascape

@Akascape I am currently not working on it as I have another major project that I am working on. The major problem with adding audio would be syncing audio with video. The audio would have to run on a different thread. If you want audio you can try the code in the following link: https://stackoverflow.com/a/69041999/15993687 I have kept this issue so I can come back to it when I have enough time.

PaulleDemon avatar May 31 '22 07:05 PaulleDemon

@Akascape In the meantime can you check this algorithm?

sec = (1/frame_rate) * number_of _frames_to_skip

You can get the frame_rate from the video_info() method you can then pass the sec to seek() method. Let me know if it works

Alright Try this then and let me know.

I tried this in the test video player, I just created a new seek frame button and added the lines as you mentioned. screenshot

Though it is seeking to the part based on the frame number, but I think it doesn't seek to the exact frame as only integer value can be passed to the seek() method.

But I hope you can do something...

Akascape avatar May 31 '22 08:05 Akascape

@Akascape Unfortunately the underlying library pyav that this library is using doesn't allow us to pass floating points to the seek method: https://pyav.org/docs/develop/api/container.html#av.container.InputContainer.seek. The closest answer that I got is https://stackoverflow.com/a/17549987/15993687 , but it's written for c, translating it might be a hassle for me. Let me know if you have any solutions in mind. I'll keep this issue open and if I find any resources that can help me I'll update the library.

PaulleDemon avatar May 31 '22 13:05 PaulleDemon

Hello @Akascape, You can surely make a progress bar based on frames, we have <<FrameGenerated>> virtual event that is generated whenever a new frame is read from the file. The ability to seek to specific frame was removed from version 2.0.0 . I would strongly recommend not to downgrade from 2.0.0 as there were memory problems with prior versions of 2.0.0. If you can wait till tomorrow I can come up with something to seek specific frames, as I am busy with some work now. Thank you.

How it was done in the older versions?

Akascape avatar May 31 '22 14:05 Akascape

BTW, how can we remove the video after using stop() method? When I close the video then the video frame is not removed and stays there in top of the label.

Akascape avatar May 31 '22 14:05 Akascape

@Akascape In the previous versions, the entire video was loaded into the memory, this caused memory issues even a small video file of 16 MB took around 3 GB of memory space, so it was removed.

As for the removal of the image after stop, try the below, Now you probably need to unbind the ended event depending on what triggered it.

import tkinter as tk
from tkVideoPlayer import TkinterVideo
from PIL import ImageTk, Image

def ended(e):
    # tkvideo.config(image="")
    tkvideo._current_img = Image.new("RGBA", tkvideo.video_info()["framesize"], (255, 0, 0, 0))
    tkvideo.config(image=ImageTk.PhotoImage(tkvideo._current_img))

def skip():
    tkvideo.stop()

root = tk.Tk()

tkvideo = TkinterVideo(scaled=True, master=root)
tkvideo.load(r"test.mp4")
tkvideo.pack(expand=True, fill="both")

tkvideo.play() # play the video
tkvideo.bind("<<Ended>>", ended)
btn = tk.Button(master=root, text="Stop", command=skip)
btn.pack()

root.mainloop()

PaulleDemon avatar May 31 '22 17:05 PaulleDemon

Ok, thanks again for the help...

Akascape avatar May 31 '22 17:05 Akascape

@Akascape Unfortunately the underlying library pyav that this library is using doesn't allow us to pass floating points to the seek method: https://pyav.org/docs/develop/api/container.html#av.container.InputContainer.seek. The closest answer that I got is https://stackoverflow.com/a/17549987/15993687 , but it's written for c, translating it might be a hassle for me. Let me know if you have any solutions in mind. I'll keep this issue open and if I find any resources that can help me I'll update the library.

@PaulleDemon Can we add a different seek function that can take float values. I don't know much about the pyav stuff but as you provided the documentation of pyav, I saw some parameters in seek method like any_frame, stream etc which says that it can seek to any frame of the video. Maybe we can transfer float values after making any_frame=True?

Akascape avatar May 31 '22 17:05 Akascape

@Akascape Unfortunately that won't work as seek parameter only accepts integer values. It will raise errors if a floating value is passed. Also, note I have updated the previous code that removes image from the label after stopping. The update prevents label from going back to previous image when resized.

PaulleDemon avatar Jun 01 '22 00:06 PaulleDemon

As there is no other seek method that takes us to particular frame, for now I will just change the whole frame stuff to seconds.

Akascape avatar Jun 01 '22 06:06 Akascape

I had the same problem. My goal is to take screenshots/images from the current video position up to the next Y seconds, with X images taken per second.

It feels more than dirty, but this is my solution. Internally, tkVideoPlayer get's the next frame from current position with:

vid_player = TkinterVideo(scaled=True, master=root, keep_aspect=True)
frame = next(vid_player._container.decode(video=0))

So I got the current frame and looping over until I get the desired one:

frame = next(vid_player._container.decode(video=0))
for _ in range(skip_amount): # skipping to desired frame
            frame = next(vid_player._container.decode(video=0))
img = frame.to_image()

After that, I have the wanted frame.

Hope that help's a bit. Im sure there is a better way to do it (without getting every 'next' frame, but I failed to do it with itertools.islice())

blankschyn avatar Nov 21 '22 13:11 blankschyn

I've noticed that you multiply the _seek_sec by 1 000 000. I assume that this is to convert to microseconds, you have stated that it is to correct the frame.

self._container.seek(self._seek_sec*1000000 , whence='time', backward=True, any_frame=False) # the seek time is given in av.time_base, the multiplication is to correct the frame
self._frame_number = self._video_info["framerate"] * self._seek_sec

If this is the case, would it not be possible to convert the seek method to take an microsecond: int parameter, instead of sec: int?

DylanKirbs avatar Dec 24 '22 10:12 DylanKirbs

@DylanKirbs I am a little busy ryt now, Can you check this out in the mean time and let me know if you still have questions: https://pyav.org/docs/develop/api/container.html#av.container.InputContainer.seek and https://pyav.org/docs/develop/api/time.html

Thank you.

PaulleDemon avatar Dec 24 '22 10:12 PaulleDemon