Skip to content
Open
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: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,5 @@ wheelbuild/
wheelhouse/
__pycache__
*.whl

*.h264
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ set(VTKSTREAMING_USE_LIBVPX ON)
set(modules
VTKStreaming::Core
VTKStreaming::Encode
VTKStreaming::Decode
VTKStreaming::NvEncode
VTKStreaming::libvpx
VTKStreaming::VpxEncode
VTKStreaming::VpxDecode
VTKStreaming::OpenGL2
VTKStreaming::WEBM
)
Expand Down
77 changes: 5 additions & 72 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,78 +89,11 @@ written to `wheelhouse/` and can be installed directly:
pip install wheelhouse/vtk_streaming-*.whl
```

## Example

```py
from vtkmodules.vtkCommonCore import vtkUnsignedCharArray
from vtkmodules.vtkRenderingCore import vtkRenderer, vtkRenderWindow, vtkRenderWindowInteractor
from vtkmodules.util.numpy_support import vtk_to_numpy
from vtkmodules.util.misc import calldata_type
from vtkmodules.util.vtkConstants import VTK_OBJECT

from vtk_streaming.vtkStreamingEncode import vtkVideoEncoder
from vtk_streaming.vtkStreamingNvEncode import vtkNvEncoderGL
from vtk_streaming.vtkStreamingVpxEncode import vtkVpxEncoder
from vtk_streaming.vtkStreamingOpenGL2 import vtkOpenGLVideoFrame
from vtk_streaming.vtkStreamingCore import VTKVC_H264, VTKVC_H265, VTKVC_VP9, VTKPF_IYUV, vtkCompressedVideoPacket

ren = vtkRenderer()
ren.SetBackground(0.1, 0.2, 0.4)
win = vtkRenderWindow()
win.AddRenderer(ren)
width, height = 641, 953 # size of video frames will likely be aligned to some value such as %4 or %8
win.SetSize(width, height)
iren = vtkRenderWindowInteractor()
iren.SetRenderWindow(win)
iren.Initialize()
iren.Render()

# Encoder takes the window to get the OpenGL context from it
encoder: vtkVideoEncoder = None
if vtkNvEncoderGL.CheckAvailability():
encoder = vtkNvEncoderGL()
encoder.SetCodec(VTKVC_H264)
print("Using H264 through NVENC")
else:
encoder = vtkVpxEncoder()
encoder.SetCodec(VTKVC_VP9)
print("Using VP9 through libvpx")
encoder.SetGraphicsContext(win)
encoder.SetWidth(width) # to handle in resize event!
encoder.SetHeight(height)
encoder.SetInputPixelFormat(VTKPF_IYUV)

# This is used to copy the framebuffer of the window to a NVENC shared-texture
picture = vtkOpenGLVideoFrame()
picture.SetContext(win)
picture.SetWidth(width) # to handle in resize event!
picture.SetHeight(height)
picture.SetPixelFormat(VTKPF_IYUV)
picture.AllocateDataStore()

# You will receive video packets through this callback
@calldata_type(VTK_OBJECT)
def receive_data(_obj: vtkVideoEncoder, _ev: int, data: vtkCompressedVideoPacket):
frame_data: vtkUnsignedCharArray = data.GetData()
raw_bytes = vtk_to_numpy(frame_data).tobytes() # do something with it
encoder.AddObserver(vtkVideoEncoder.EncodedVideoChunkEvent, receive_data)

# We have logic to insert in rendering loop,
# Depending on the application and used GUI etc, this might differ.
while True:
# Render current frame
iren.ProcessEvents()
iren.Render()
# Capture last framebuffer
picture.Capture(win)
# Encode using choosen backend, this may invoke EncodedVideoChunkEvent any number of times
encoder.Encode(picture)

# In a real application this should be called on exit
# this example is simply killed by user with ctrl+c
enc.Drain()
enc.Shutdown()
```
## Examples

1. [examples/simple_encoder_decoder.py](./examples/simple_encoder_decoder.py) - Live VP9 encode/decode round-trip with two render windows side by side.
2. [examples/resize_encoder_decoder.py](./examples/resize_encoder_decoder.py) - VP9 encode/decode round-trip that survives window resizes.
3. [examples/simple_nvenc_record.py](./examples/simple_nvenc_record.py) - Record a render window for later playback using NVENC. This needs `ffplay` to playback the .h264 file.

## Getting help

Expand Down
14 changes: 0 additions & 14 deletions Streaming/Encode/vtkVideoEncoder.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -92,20 +92,6 @@ vtkRenderWindow* vtkVideoEncoder::GetGraphicsContext() const
return this->GraphicsContext;
}

//------------------------------------------------------------------------------
void vtkVideoEncoder::SetWidth(int width)
{
vtkLogScopeF(TRACE, "%s, w=%d", __func__, width);
this->Width = width;
}

//------------------------------------------------------------------------------
void vtkVideoEncoder::SetHeight(int height)
{
vtkLogScopeF(TRACE, "%s, h=%d", __func__, height);
this->Height = height;
}

//------------------------------------------------------------------------------
void vtkVideoEncoder::SetBitRateControlMode(int mode)
{
Expand Down
4 changes: 2 additions & 2 deletions Streaming/Encode/vtkVideoEncoder.h
Original file line number Diff line number Diff line change
Expand Up @@ -161,9 +161,9 @@ class VTKSTREAMINGENCODE_EXPORT vtkVideoEncoder : public vtkObject
/**
* Set/Get width and height of encoding context.
*/
void SetWidth(int width);
vtkSetMacro(Width, int);
vtkGetMacro(Width, int);
void SetHeight(int height);
vtkSetMacro(Height, int);
vtkGetMacro(Height, int);
///@}

Expand Down
26 changes: 12 additions & 14 deletions Streaming/NvEncode/vtkNvEncoderGL.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -160,28 +160,26 @@ bool vtkNvEncoderGL::InitializeInternal()
unsigned int cudaDeviceCount = 0;
VTK_NV_CUDA_DRIVER_API_CHECKED_INVOKE(
cuGLGetDevices_v2(&cudaDeviceCount, devices, 4, CU_GL_DEVICE_LIST_ALL));
char devName[100];
if (!cudaDeviceCount)
{
vtkLogF(ERROR, "OpenGL rendering is not on a CUDA device.");
return false;
}
else
{
vtkLogF(INFO, "Found %u devices capable of CUDA-OpenGL interop.", cudaDeviceCount);
status = cufns->cuDeviceGetName(devName, sizeof(devName), devices[0]);
auto& ctx = this->CUDAInstance->Context;
ctx = nullptr;
status = cufns->cuCtxCreate_v2(&ctx, 0, devices[0]);
unsigned int version = 0;
cufns->cuCtxGetApiVersion(ctx, &version);
unsigned int major = version / 1000;
unsigned int minor = version - major * 1000;
vtkLogF(TRACE,
"NVENC GPU #%d ('%s') CUDA ctx %d.%d in use. %d gpu (s) capable of cuda-gl interop.", 0,
devName, major, minor, cudaDeviceCount);
}
char devName[100];
status = cufns->cuDeviceGetName(devName, sizeof(devName), devices[0]);
vtkLogF(INFO, "NvEncode: GPU %d in use - %s", 0, devName);

auto& ctx = this->CUDAInstance->Context;
ctx = nullptr;
status = cufns->cuCtxCreate_v2(&ctx, 0, devices[0]);

unsigned int version = 0;
cufns->cuCtxGetApiVersion(ctx, &version);
unsigned int major = version / 1000;
unsigned int minor = version - major * 1000;
vtkLogF(INFO, "CUDA context in use - %d.%d", major, minor);
}

// 2. Initializes NVENC with CUDA device.
Expand Down
1 change: 0 additions & 1 deletion Streaming/NvEncode/vtkNvEncoderInternals.cxx
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,6 @@ bool vtkNvEncoderInternals::LoadNvEncodeAPI()
}
else
{
vtkLog(INFO, << "loaded NvEncodeAPI.");
return true;
}
}
Expand Down
196 changes: 196 additions & 0 deletions examples/resize_encoder_decoder.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
"""VP9 encode/decode round-trip that survives window resizes.

Same side-by-side layout as examples/simple_encoder_decoder.py: the left
window renders a spinning cylinder, every finished render (vtkCommand::
EndEvent) is captured and VP9-encoded, and each packet is decoded and
displayed in the right window.

The addition is resize handling, and it is almost free: whenever the scene
window's size changes, the capture frame is rebuilt at the new size — the
next Encode then makes vtkVideoEncoder tear down and rebuild its context
from the new frame dimensions, and the decoder follows the packet dimensions
on its own. Only the output window needs an explicit SetSize to match.

Drag-resize the left window, or just wait: a timer cycles it through a few
sizes automatically.
"""

from datetime import datetime

import vtkmodules.vtkRenderingOpenGL2 # noqa: F401 (register the OpenGL factory)
import vtkmodules.vtkInteractionStyle # noqa: F401 (register interactor styles)
from vtkmodules.util.misc import calldata_type
from vtkmodules.util.vtkConstants import VTK_OBJECT
from vtkmodules.vtkCommonCore import vtkCommand
from vtkmodules.vtkFiltersSources import vtkCylinderSource
from vtkmodules.vtkRenderingCore import (
vtkActor,
vtkPolyDataMapper,
vtkRenderer,
vtkRenderWindow,
vtkRenderWindowInteractor,
vtkTextActor,
)

from vtk_streaming.vtkStreamingCore import (
VTKPF_IYUV,
VTKVC_VP9,
vtkCompressedVideoPacket,
vtkRawVideoFrame,
)
from vtk_streaming.vtkStreamingDecode import vtkVideoDecoder
from vtk_streaming.vtkStreamingEncode import vtkVideoEncoder
from vtk_streaming.vtkStreamingOpenGL2 import vtkOpenGLVideoFrame
from vtk_streaming.vtkStreamingVpxDecode import vtkVpxDecoder
from vtk_streaming.vtkStreamingVpxEncode import vtkVpxEncoder

# The automatic size cycle; codecs prefer sizes aligned to %4 or %8.
SIZES = [(640, 480), (800, 600), (480, 360)]
TICKS_PER_SIZE = 120 # timer ticks between automatic resizes (~4 s at 33 ms)

width, height = SIZES[0]

# Left window: the scene that gets encoded.
cylinder = vtkCylinderSource()
mapper = vtkPolyDataMapper()
mapper.SetInputConnection(cylinder.GetOutputPort())
actor = vtkActor()
actor.SetMapper(mapper)
actor.GetProperty().SetColor(255 / 255, 99 / 255, 71 / 255) # tomato
actor.RotateX(30.0)
actor.RotateY(-45.0)
renderer = vtkRenderer()
renderer.AddActor(actor)
renderer.SetBackground(0.1, 0.2, 0.4)

def current_time_text() -> str:
now = datetime.now()
return f"{now:%H:%M:%S}.{now.microsecond // 1000:03d}"


# Time readout (HH:MM:SS.ms), anchored to the top right corner of the scene
# window. Normalized viewport coordinates keep it in the corner across resizes.
frame_text = vtkTextActor()
frame_text.SetInput(current_time_text())
frame_text.GetTextProperty().SetFontSize(18)
frame_text.GetTextProperty().SetJustificationToRight()
frame_text.GetTextProperty().SetVerticalJustificationToTop()
frame_text.GetPositionCoordinate().SetCoordinateSystemToNormalizedViewport()
frame_text.GetPositionCoordinate().SetValue(0.98, 0.98)
renderer.AddViewProp(frame_text)

scene_window = vtkRenderWindow()
scene_window.SetWindowName("Input scene (VP9 encode)")
scene_window.AddRenderer(renderer)
scene_window.SetSize(width, height)
scene_window.SetPosition(50, 50)

interactor = vtkRenderWindowInteractor()
interactor.SetRenderWindow(scene_window)
interactor.Initialize()
scene_window.Render()

# Right window: displays whatever comes out of the decoder.
decoded_window = vtkRenderWindow()
decoded_window.SetWindowName("Decoded output (VP9 decode)")
decoded_window.SetSize(width, height)
decoded_window.SetPosition(50 + max(w for w, _ in SIZES) + 20, 50)
decoded_window.Render() # initialize its OpenGL context before first use

decoder = vtkVpxDecoder()
decoder.SetGraphicsContext(decoded_window)


@calldata_type(VTK_OBJECT)
def display_frame(_decoder: vtkVideoDecoder, _event: int, frame: vtkOpenGLVideoFrame):
# frame.Render presents into the window (it calls window->Frame() itself).
frame.Render(decoded_window)


decoder.AddObserver(vtkVideoDecoder.DecodedVideoFrameEvent, display_frame)

encoder = vtkVpxEncoder()
encoder.SetGraphicsContext(scene_window)
encoder.SetCodec(VTKVC_VP9)
encoder.SetWidth(width)
encoder.SetHeight(height)
encoder.SetInputPixelFormat(VTKPF_IYUV)


@calldata_type(VTK_OBJECT)
def receive_packet(
_encoder: vtkVideoEncoder, _event: int, packet: vtkCompressedVideoPacket
):
# In a real application this bitstream would travel over the network;
# here it goes straight to the decoder. Each packet carries its display
# dimensions, which is how the decoder picks up a resize.
decoder.Decode(packet)


encoder.AddObserver(vtkVideoEncoder.EncodedVideoChunkEvent, receive_packet)


def make_picture(window, picture_width, picture_height):
picture = vtkOpenGLVideoFrame()
picture.SetContext(window)
picture.SetWidth(picture_width)
picture.SetHeight(picture_height)
picture.SetPixelFormat(VTKPF_IYUV)
picture.SetSliceOrderType(vtkRawVideoFrame.TopDown)
picture.AllocateDataStore()
return picture


picture = make_picture(scene_window, width, height)


def update_time_text(_window: vtkRenderWindow, _event: int):
frame_text.SetInput(current_time_text())


def encode_frame(window: vtkRenderWindow, _event: int):
global picture
size = tuple(window.GetSize())
if size != (picture.GetWidth(), picture.GetHeight()):
# Rebuild the capture frame; the first Encode at the new dimensions
# makes the encoder rebuild its context, and the decoder follows the
# packet dimensions. Only the output window needs explicit resizing.
print(f"resized to {size[0]}x{size[1]}")
picture = make_picture(window, *size)
decoded_window.SetSize(*size)
picture.Capture(window)
encoder.Encode(picture) # fires EncodedVideoChunkEvent per packet
# Decoding rendered into the other window; hand the context back.
window.MakeCurrent()


# StartEvent fires at the start of every vtkRenderWindow::Render, so the
# timestamp is current in the frame about to be drawn and encoded;
# EndEvent fires at the end.
scene_window.AddObserver(vtkCommand.StartEvent, update_time_text)
scene_window.AddObserver(vtkCommand.EndEvent, encode_frame)

tick = 0


def spin_and_cycle_size(_interactor: vtkRenderWindowInteractor, _event: int):
global tick
tick += 1
renderer.GetActiveCamera().Azimuth(1.0)
if tick % TICKS_PER_SIZE == 0:
scene_window.SetSize(*SIZES[(tick // TICKS_PER_SIZE) % len(SIZES)])
scene_window.Render()


interactor.AddObserver(vtkCommand.TimerEvent, spin_and_cycle_size)
interactor.CreateRepeatingTimer(33)

print("Drag-resize the left window, or wait for the automatic size cycle.")
print("Press 'q' or 'e' in the left window to quit.")
interactor.Start()

encoder.Drain()
encoder.Shutdown()
# Note: do not call decoder.Drain(); vtkVpxDecoder::SendEOS forwards a null
# packet that DecodeInternal dereferences. Shutdown is safe.
decoder.Shutdown()
Loading
Loading