Repository navigation
Conversation
|
I just realised that the crop option in the demo app doesnt work properly with this new renderer. Edit: From what I can see, |
media3 1.11.1 require minSdk 23 and more.
Add a new AssRenderType, EFFECTS_ATLAS, which draws ASS subtitles with OpenGL ES directly onto each decoded video frame in media3's video effects pipeline. Other modes composite the libass images into a full-frame RGBA bitmap on the CPU and upload it on every frame. EFFECTS_ATLAS instead packs the ASS_Image bitmaps into 8-bit atlas textures and uploads only the packed area. It draws each image as a quad with its color as a vertex attribute, so each atlas page takes one draw call. A bitmap that appears several times in a frame is stored only once per page. It also has unit tests that you can run on non-android device. Limitations: 1. HDR and Dolby Vision video are not supported. 2. It always render at the video size (not the screen size).
A new overlay starts with an empty atlas, but it only updated the atlas when ass_render_frame reported a change. libass compares with the last frame its renderer rendered, which can come from an overlay that was released since. When the subtitles were turned off and on again during a static line, the new overlay got changed == 0 and drew nothing until the subtitles changed. The overlay now records whether it has rendered a frame, and its first frame always fills the atlas. AssOverlayTest.NewOverlayDrawsUnchangedSubtitles covers it.
Workaround this issue: androidx/media#2727
56154e1 to
14d1108
Compare
|
|
||
| ### Limitations | ||
|
|
||
| - It doesn't support HDR or Dolby Vision video. |
There was a problem hiding this comment.
I forgot to add that it render to the video resolution (not the screen size).
|
@skoruppa I just saw that NuvioTV switched from libass-android to your implementation in NuvioMedia/NuvioTV#4165 I find it kinda funny that both PR (mine and yours) use the same approach (an atlas) and almost have been made in same time. I was wondering if you tested this PR? Do you know if yours perform better (I haven't check the detail, but I was just wondering).? |
|
Hey! Yeah, both landed on an atlas approach around the same time ;) The main difference is the rendering path: ours draws on a separate SurfaceView overlay (pinned to the video frame's vsync via eglPresentationTimeANDROID), while yours draws directly onto the video frame through setVideoEffects/GlShaderProgram. Both use atlas packing, but yours has the native C packing (AssSkyline) with PBO uploads on GLES3, which is neat. Your EFFECTS_ATLAS approach is actually really interesting for me - we have a subtitle timing issue on some Android TVs where the overlay SurfaceView goes through a shorter display pipeline than the video surface (TV image processing adds latency to the video path only), causing subtitles to appear ~7 frames early. Compositing directly onto the video frame like your PR does would eliminate that entirely. I need to check it. Performance-wise, our overlay SurfaceView path works well on both phones and TVs (the rendering is off the main thread, speculative render-ahead for heavy typesetting, etc), but your in-place draw avoids the second surface entirely which is cleaner. The tradeoff is HDR/DV support - our overlay path handles that since it's a separate layer, but for SDR content maybe yours would be the better approach? Will do some testing ;) |
Yes, that's a big advantage of
That sounds really interesting. I should think about doing that (but probably in another PR). The main problem I see with that is that we would need to do a copy of libass bitmap to "cache" them and it would take more memory usage.
There is probably a way to support HDR/DV with my approach, but I don't know well how HDR/DV works and I don't have the material to test it, so that's why I didn't really tried to implement it.
Thanks! |
|
I tested your EFFECTS_ATLAS approach on my TCL TV and it works great - subtitle timing is perfect, exactly as you described One issue I ran into: on heavy typesetting (karaoke with many quads), the synchronous rendering on the GL thread was causing video frame drops on this TV. Since ass_render_frame + atlas pack + upload all happen inside queueInputFrame, a slow libass render (30-80ms) would exceed the 42ms frame budget and stall the video pipeline. I solved this by running the libass render + atlas GL drawing on a dedicated thread with a shared EGL context. The render thread writes to a shared RGBA texture, and the video effect's GL thread blends it onto each video frame with a single fullscreen quad. This way libass never blocks video delivery - same model as the overlay SurfaceView but composited onto the frame. I used glFenceSync/glClientWaitSync for cross-context texture visibility. On the speculative render-ahead: we ended up not needing it for this approach since the render thread runs independently - if libass is slow, the video thread just redraws the last cached texture. Subtitle animation may drop to a lower framerate but video stays smooth Would you be OK with me shipping a version in NuvioTV that builds on your EFFECTS_ATLAS PR? I would credit you our libass lib. The core atlas + GL code from your PR is really well done and I rather build on it than reimplement from scratch ;) |
Render libass subtitles directly onto each video frame's texture using setVideoEffects with an async shared EGL context. Both the video and subtitles travel through the TV's display processing pipeline together, eliminating the subtitle timing offset on Android TV. Architecture: - AssVideoEffectAsync (GlEffect/GlShaderProgram) blends a cached RGBA texture onto each video frame with a single fullscreen quad (<0.2ms) - Dedicated render thread with shared EGL context runs libass + atlas GL drawing into the shared texture, never blocking video delivery - AssFrameTimeMapper + AssVideoRendererWrapper track stream offsets for precise media time mapping from video effect PTS - Per-context FBOs with glFenceSync for cross-context texture visibility Features: - Multi-page atlas with per-page GL textures - GLES3 R8 textures with swizzle, GLES2 ALPHA fallback - GL_LINEAR filtering for smooth render scale downsampling - Render scale support via AssAtlasPipelineConfig.renderScale - GL error draining to prevent media3 playback failures - highp fragment shader precision for large atlas UV accuracy Limitation: - setVideoEffects forces DefaultVideoFrameProcessor which tone-maps HDR to SDR on some TVs. HDR content needs overlay SurfaceView fallback. Credits: - Inspired by peerless2012/libass-android PR #90 by moi15moi (peerless2012/libass-android#90)
|
Yes, I think the render shoul work on a thread and set a timeout, just like other render type do. or it will block the video frame. |
|
I investigated a bit and the problem with video effects approach is that HDR just can't work - using them will always tone map video to SDR. Because of that I think I will leave both methods in Nuvio ;) and let users decide what is more important for them, frame perfect ass subs or hdr with ass subs but with suboptimal sync |
We can add OverLaySurface to render ass in a SurfaceView, that maybe useful. |
|
Update after more testing: the video effects path has more limitations than just HDR:
Because of these issues the default mode in Nuvio will be Overlay. Video Effect is available as an opt-in for users who need frame-accurate ASS timing on 24fps SDR content and accept the tradeoffs. The player only activates DVFP when an ASS track is actually selected (lazy activation), so non-ASS content never pays the overhead even with Video Effect mode enabled |
|
Thanks for testing all of this! The hardware you're testing on seems REALLY weak. If turning on DefaultVideoFrameProcessor with a passthrough effect is enough to drop frames on 60fps video, the GPU can't even manage the bare minimum DVFP does at 60fps: sample the decoded frame, draw it into a texture, then draw it again onto the output surface. Was that 4K60 or 1080p60? I checked why setVideoEffects with no effects seems to affect your device so much. Without setVideoEffects In brief, the app does no GPU work and makes no copies of the frame. With setVideoEffects
In brief, the GPU draws every full frame twice and the display receives an RGBA buffer made by the GPU instead of the decoder's YUV buffer. Have you tried mpv on your TV? I am currently trying to find an alternative solution, but I don't think there is a solution that would allow us to be frame accurate (which, for me, is extremely important). I don't care if the video lags due to the subtitle. I prefer subtitle accuracy over lag. |
|
The test was 1080p60 anime on a TCL MediaTek TV (API 34, Mali-G52). Not a flagship but more capable than most TV boxes our users run (Amlogic S905X, Rockchip, older Fire TV sticks). If DVFP can't hold 1080p60 passthrough on this hardware, it definitely won't work on weaker devices. It was on a release build. The implementation I got runs better than mpv on my device which is good :D |
Are you talking about this PR or what you did in NuvioMedia/nuvio-libass#1 ? |
|
I think the async render and not block or block with a timeout is a better way. Maybe we just move render and copy the pieces memory to page on a thread, upload tex and render still work on player thread. As the most heavy work is libass render, not the gl render. |
I don't agree. I believe it's better to lag then to skip subtitles.
The gl can take a signicative amount of time when there is a lot of bitmap. |
|
Can you test this file ass test? |
This PR partially fix #71
This new render type draws ASS subtitles with OpenGL directly onto each decoded video frame, inside media3's video effects pipeline. It is much faster then the other render type because, first of all, it use the gpu to blend libass bitmap. Secondly, instead of uploading each libass bitmap to the gpu, it create an atlas to send has many bitmap as possible to the gpu.
It is much more complex, but it helps a lot to improve the performance. From what I could see, with my phone, I can now play really heavy .ass subtitle and we have similar performance to mpv.
I tested it with these 3 samples on my phone which are heavy (thanks to @witchymary for providing them):
To be sure it is frame accurate, I used this sample (frame_accurate.zip). Each frame goes from black to white to black to white. On each black frame, there is a .ass white draw. Like that, when playing the video, you should only see white frame. If you see any black frame, there is a problem with the accuracy.
I added many unit tests for this new render type to cover has many case as possible.
You can run those tests on a non-android device (the CI runs those tests).
Note that I did use Claude to create this PR.