Embed Video into GUI - Python API
-
Hi, I was wondering if anyone ever tried embedding a video into the GUI via Python?
One method I was thinking of was to use OpenCV for the video backend, while getting the frames one-by-one and draw it into an UserArea.
-
Hey @ezeuz,
Thank you for reaching out to us. I do not know if someone tried doing this with a user area (UA), but here are a few thoughts:
- OpenCV seems a bit overkill for the backend, you could just use c4d.BaseBitmap.InitWith to load your video files. Unless you plan to display video files in formats that Cinema 4D cannot load.
- While your approach is in general correct, you seem to overlook something that many people tend to overlook: All drawing functions in Cinema 4D, just as in pretty much every other API, are not called for each frame. Instead they act as a buffer into which you put drawing instructions which then are carried out by Cinema 4D in an optimized manner, i.e., stuff is being cached. The applies to everything from drawing into a viewport to drawing into the canvas of a dialog/UA.
- So, when you do not take care of this, Cinema 4D would draw your UA once and then cache that bitmap. After that it would never call you again until the user resizes the parent of the UA or otherwise makes this bitmap invalid.
- The normal way of invalidating a UA are user inputs, the user either clicks on the UA or types something while the UA is focused, and in the event handling for that we then force a redraw. For displaying a video this strategy does not work as we do not have any events being fired for each frame by Cinema 4D where we could call
c4d.gui.GeUserArea.Redraw()
to force a redraw. - The solution to this is to use a timer, which is a feature of the
c4d.gui.GeUserArea
class. You can start a timer withc4d.gui.GeUserArea.SetTimer()
and then you will receive ac4d.gui.GeUserArea.Timer()
call for each timer event, which you can use to update your video frame. - The problem with this is that
GeDialog/GeUserArea.Timer
does not offer arbitrary precision, nor high precision in general. To display 30FPS, you would need a timer event every 33.33ms. When you set the timer that low, you will have quite a bit of error. You can adapt to this, but this might result in a bit of jitter in the video playback. - Finally, and this is the most important point, Python is a terrible choice to draw something with 30 FPS or even more to the screen. When you design your code carefully, i.e., your
GeUserArea.DrawMsg
should be hyper-optimized, most of the code will actually run in the C++ backend and this could work. But this can easily perform very poorly.
Cheers,
FerdinandResult
Timer fired after 0.000 seconds Timer fired after 0.070 seconds Timer fired after 0.047 seconds Timer fired after 0.047 seconds Timer fired after 0.048 seconds Timer fired after 0.061 seconds Timer fired after 0.046 seconds Timer fired after 0.047 seconds Timer fired after 0.051 seconds Timer fired after 0.011 seconds Timer fired after 0.055 seconds Timer fired after 0.043 seconds
Code
"""Simple example of a GeDialog that uses a timer to update itself. """ import c4d import time class MyDialog(c4d.gui.GeDialog): """Implementation of a GeDialog that uses a timer to update itself. """ def __init__(self): """Constructor. Initializes the last tick time and the count of timer firings. """ self._lastTick: float | None = None self._count: int = 0 def CreateLayout(self) -> bool: """Called by Cinema 4D to let the dialog populate its layout. """ self.SetTitle("Timer Example") self.AddStaticText(1000, c4d.BFH_SCALEFIT, name="I have no controls, just a timer") self.SetTimer(33) # Set the timer to fire every 33ms return True def Timer(self, msg: c4d.BaseContainer) -> None: """Called by Cinema 4D when the timer fires. The point of this example is to demonstrate that the ticks are not as regular as one might expect. When I ran this example, I got the following output: Timer fired after 0.000 seconds Timer fired after 0.070 seconds Timer fired after 0.047 seconds Timer fired after 0.047 seconds Timer fired after 0.048 seconds Timer fired after 0.061 seconds Timer fired after 0.046 seconds Timer fired after 0.047 seconds Timer fired after 0.051 seconds Timer fired after 0.011 seconds Timer fired after 0.055 seconds Timer fired after 0.043 seconds The first 0.000 is okay because it's the first tick. But the rest of the ticks are not regular spaced 33ms ticks although that is what we asked for. As rule of thumb, timers below 100ms are getting increasingly relatively inaccurate, and for all timer events you can expect a margin of error of about 50ms. This can get worse when Cinema 4D is under heavy load. What is mitigating this a bit is that we use here Python's time.perf_counter() function to measure time which despite its name is not a high-resolution timer. So, the actual error is a bit lower. """ t: float = time.perf_counter() delta: float = t - self._lastTick if self._lastTick else 0 self._lastTick = t print(f"Timer fired after {delta:.3f} seconds") self._count += 1 if self._count > 10: self.SetTimer(0) # Stop the timer def main(): dialog = MyDialog() dialog.Open(c4d.DLG_TYPE_MODAL) # Open the dialog # Execute main() if __name__=='__main__': main()
-
@ferdinand Thanks a lot on the feedbacks! Yeah I was thinking of using external library coz I was used to Maya, and the media support in that app was horrendous. I guess even Audio support-wise, Cinema 4D is miles better out-of-the-box. Hopefully c4d fits with our usecase.
Ahhh I see, good point on the Python & GUI limitations. Thinking back, it makes sense that high-performance GUI rarely rendered/looped for each frame. Our use case was more like generating some kind of animation based on a video. The video is there for just for side-by-side preview.
So all in all, we don't really need highly accurate playback. By using the time delta instead of raw timer and skipping frames here and there, overall I presume it'll be fine (say, even 10 FPS out of a 30 FPS video). But those are something I hadn't thought about, thanks a ton once again!
-
Hey @ezeuz,
I guess even Audio support-wise
I am not quite sure how this is meant, but
BaseBitmap
is just an interface for pixel content (which despite its a bit misleading name can also be used for video content). All audio information will be ignored byBaseBitmap
.Cheers,
Ferdinand -
@ferdinand Oh I was just making a parallel, how Cinema 4D supports more audio nicely by default (compared to Maya), so I can generally expect Cinema 4D supports a lot more media compared to Maya (which I had experience developing a plugin on before moving to C4D).
-
Hey,
okay, good to know. I just wanted to make clear that
BaseBitmap
does not support audio playback or sampling audio information. Except for theSoundEffectorCustomGui
, there is in fact no sound support at all in the Python API. The C++ API offers more support with the MEDIASESSION namespace, but also this is mostly geared towards writing such information.Cheers,
Ferdinand