diff options
author | Sven Göthel <[email protected]> | 2024-02-02 08:27:49 +0100 |
---|---|---|
committer | Sven Göthel <[email protected]> | 2024-02-02 08:27:49 +0100 |
commit | 23cf5279472d3ae1b2d8d1904e6b1f1e7fd8f012 (patch) | |
tree | 7f943da4a1a6c8fd917eff160b45b700a125f258 /src/graphui | |
parent | 9ff736464e0d2762c424bab66bc6d03ccc6e6d11 (diff) |
Bug 1494 - GLMediaPlayer/GraphUI: Support Displaying Bitmap'ed Subtitles (PGS ..) via FFMPEGFMediaPlayer/FFmpeg
FFMPEGFMediaPlayer related changes:
- Add libswscale (6th FFmpeg lib used) for sws_getCachedContext(), sws_scale() and sws_freeContext(),
used natively to convert the palette'ed bitmap into RGBA colorspace -> GL texture
- Handling AVSubtitleRect.type SUBTITLE_BITMAP
-- only handled if libswscale is available
-- config/adjust texture object
-- sws_scale palette'ed bitmap to texture
-- intermediate memory is cached, may be resized and free'ed at destroy
-- texture objects are managed and passed from GLMediaPlayerImpl,
as they are also forwarded to player client via SubBitmapEvent
- Passing the AVCodecID to GLMediaPlayerImpl, converted to our CodecID enum.
- Unifying creation and opening of AVCodecContext with 'createOpenedAVCodecContext(..)'
+++
SubtitleEvent*
- SubTextEvent now also handles ASS.Dialogue (FFmpeg 4)
besides ASS.Event (FFmpeg 5, 6, ..).
+++
GLMediaPlayerImpl
- Added ringbuffer subTexFree, managing Texture for bitmap'ed subtitles
-- Uses 1 bitmap-subtitle Texture per used textureCount in cache,
as one bitmap-subtile can be displayed per frame.
Could be potentially reduced to just 2 .. but resources used are
relatively low here.
- Validating subTexFree + videoFramesFree usage,
use blocking get/put ringbuffer due to utilization from different threads.
- Receives subtitle content from native getNextPacket0() via callback,
creates SubtitleEvent instance and passes it to a SubtitleEventListener - if exists.
(See MediaButton example)
-- SubBitmapEvent also gets its special SubBitmapEvent.TextureOwner to handle client releasing
the event and allowing us to put back the Texture resource to 'subTexFree'.
This passing through of the Texture object is probably a weakness of this lifecycle
and requires the client to ensure SubtitleEvent.release() gets called.
See MediaButton example!
- Exposing CodecID, allowing clients like MediaButton to handle SubtitleEvent content according to codec
Diffstat (limited to 'src/graphui')
-rw-r--r-- | src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java | 319 | ||||
-rw-r--r-- | src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java | 86 |
2 files changed, 278 insertions, 127 deletions
diff --git a/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java b/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java index fc0a6e0b2..3de75bba6 100644 --- a/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java +++ b/src/graphui/classes/com/jogamp/graph/ui/shapes/MediaButton.java @@ -27,6 +27,7 @@ */ package com.jogamp.graph.ui.shapes; +import com.jogamp.opengl.GL; import com.jogamp.opengl.GL2ES2; import com.jogamp.opengl.GLProfile; @@ -34,22 +35,25 @@ import java.util.ArrayList; import java.util.List; import com.jogamp.common.av.AudioSink; -import com.jogamp.common.os.Clock; import com.jogamp.common.util.InterruptSource; import com.jogamp.graph.curve.Region; import com.jogamp.graph.curve.opengl.RegionRenderer; import com.jogamp.graph.font.Font; import com.jogamp.graph.ui.GraphShape; import com.jogamp.graph.ui.Scene; +import com.jogamp.math.Vec2f; import com.jogamp.math.Vec4f; import com.jogamp.math.geom.AABBox; import com.jogamp.math.util.PMVMatrix4f; -import com.jogamp.opengl.util.av.ASSEventLine; -import com.jogamp.opengl.util.av.ASSEventListener; +import com.jogamp.opengl.util.av.SubTextEvent; +import com.jogamp.opengl.util.av.SubBitmapEvent; +import com.jogamp.opengl.util.av.SubtitleEvent; +import com.jogamp.opengl.util.av.SubtitleEventListener; +import com.jogamp.opengl.util.texture.ImageSequence; +import com.jogamp.opengl.util.av.CodecID; import com.jogamp.opengl.util.av.GLMediaPlayer; import com.jogamp.opengl.util.av.GLMediaPlayer.GLMediaEventListener; import com.jogamp.opengl.util.av.GLMediaPlayer.StreamException; -import com.jogamp.opengl.util.texture.TextureSequence.TextureFrame; /** * A GraphUI {@link GLMediaPlayer} based {@link TexSeqButton} {@link GraphShape}. @@ -69,7 +73,8 @@ import com.jogamp.opengl.util.texture.TextureSequence.TextureFrame; * </p> */ public class MediaButton extends TexSeqButton { - private final boolean DEBUG_SUB = false; + private static final boolean DEBUG_SUB = false; + private static final boolean DEBUG_PGS_POSSZ = false; private boolean verbose = false; private final Label subLabel; @@ -78,24 +83,41 @@ public class MediaButton extends TexSeqButton { private float subLineHeightPct; private float subLineDY = 0.25f; private final Rectangle subBlend; - private final List<ASSEventLine> assEventQueue = new ArrayList<ASSEventLine>(); - private final Object assEventLock = new Object(); + private final ImageButton subTexImg; + /** Default text/ASS subtitle line height percentage, {@value}. */ + public static final float DEFAULT_ASS_SUB_HEIGHT = 0.075f; + /** Default text/ASS subtitle y-axis offset to bottom in line-height percentage, {@value}. */ + public static final float DEFAULT_ASS_SUB_POS = 0.25f; + /** Default color for the text/ASS subtitles, defaults to RGBA {@code 1, 1, 1, 1}. */ + public static final Vec4f DEFAULT_ASS_SUB_COLOR = new Vec4f( 1, 1, 1, 1 ); + /** Default blending alpha (darkness) for the text/ASS subtitles, defaults to {@value}. */ + public static final float DEFAULT_ASS_SUB_BLEND = 0.3f; + + private static final float ASS_SUB_USED_WIDTH = 0.90f; + private static final float ASS_SUB_BLEND_ADDED_HEIGHT = 0.25f; + + private SubtitleEvent drawLastSub; + + private final List<SubtitleEvent> subEventQueue = new ArrayList<SubtitleEvent>(); + private final Object subEventLock = new Object(); + + /** Constructs a {@link MediaButton} with subtitles disabled. */ public MediaButton(final int renderModes, final float width, final float height, final GLMediaPlayer mPlayer) { - this(renderModes, width, height, mPlayer, null, 0); + this(renderModes, width, height, mPlayer, null); } /** - * + * Constructs a {@link MediaButton} prepared for using subtitles. * @param renderModes * @param width * @param height * @param mPlayer - * @param subFont subtitle font - * @param subLineHeightPct one subtitle line height percentage of this shape, default is 0.1f + * @param subFont text/ASS subtitle font + * @see #setSubtitleParams(Font, float, float) + * @see #setSubtitleColor(Vec4f, float) */ - public MediaButton(final int renderModes, final float width, final float height, final GLMediaPlayer mPlayer, - final Font subFont, final float subLineHeightPct) + public MediaButton(final int renderModes, final float width, final float height, final GLMediaPlayer mPlayer, final Font subFont) { super(renderModes & ~Region.AA_RENDERING_MASK, width, height, mPlayer); @@ -104,7 +126,7 @@ public class MediaButton extends TexSeqButton { setToggleOffColorMod(0.8f, 0.8f, 0.8f, 1.0f); setToggleOnColorMod(1.0f, 1.0f, 1.0f, 1.0f); - mPlayer.setASSEventListener(assEventListener); + mPlayer.setSubtitleEventListener(subEventListener); final Font f; if( null != subFont ) { @@ -115,19 +137,25 @@ public class MediaButton extends TexSeqButton { subEnabled = false; } this.subZOffset = Button.DEFAULT_LABEL_ZOFFSET; - this.subLineHeightPct = subLineHeightPct; + this.subLineHeightPct = DEFAULT_ASS_SUB_HEIGHT; this.subLabel = new Label(renderModes, f, ""); - this.subLabel.setColor( new Vec4f( 1, 1, 0, 1 ) ); + this.subLabel.setColor( DEFAULT_ASS_SUB_COLOR ); this.subLabel.moveTo(0, 0, subZOffset); this.subBlend = new Rectangle(renderModes, 1f, 1f, 0f); - this.subBlend.setColor( new Vec4f( 0, 0, 0, 0.2f ) ); + this.subBlend.setColor( new Vec4f( 0, 0, 0, DEFAULT_ASS_SUB_BLEND ) ); + this.subTexImg = new ImageButton(renderModes, width, height, new ImageSequence(mPlayer.getTextureUnit(), true /* useBuildInTexLookup */)); + this.subTexImg.setPerp().setToggleable(false).setDragAndResizeable(false).setInteractive(false); + // this.subTexImg.setBorder(0.001f).setBorderColor(1, 1, 0, 1); + this.subTexImg.getImageSequence().setParams(GL.GL_LINEAR, GL.GL_LINEAR, GL.GL_CLAMP_TO_EDGE, GL.GL_CLAMP_TO_EDGE); + this.subTexImg.setARatioAdjustment(false); + this.drawLastSub = null; } /** - * Sets subtitle parameter - * @param subFont subtitle font - * @param subLineHeightPct one subtitle line height percentage of this shape, default is 1/10 (0.1f) - * @param subLineDY y-axis offset to bottom in line-height, defaults to 1/4 (0.25f) + * Sets text/ASS subtitle parameter + * @param subFont text/ASS subtitle font + * @param subLineHeightPct text/ASS subtitle line height percentage, defaults to {@link #DEFAULT_ASS_SUB_HEIGHT} + * @param subLineDY text/ASS y-axis offset to bottom in line-height, defaults to {@link #DEFAULT_ASS_SUB_POS} */ public void setSubtitleParams(final Font subFont, final float subLineHeightPct, final float subLineDY) { this.subLabel.setFont(subFont); @@ -136,23 +164,23 @@ public class MediaButton extends TexSeqButton { this.subEnabled = true; } /** - * Sets subtitle colors - * @param color color for the text, defaults to RGBA {@code 1, 1, 0, 1} - * @param blend blending alpha (darkness), defaults to 0.2f + * Sets text/ASS subtitle colors + * @param color color for the text/ASS, defaults to {@link #DEFAULT_ASS_SUB_COLOR} + * @param blend blending alpha (darkness), defaults to {@link #DEFAULT_ASS_SUB_BLEND} */ public void setSubtitleColor(final Vec4f color, final float blend) { this.subLabel.setColor( color ); this.subBlend.setColor( 0, 0, 0, blend ); } - public final ASSEventListener getASSEventListener() { return assEventListener; } - private final ASSEventListener assEventListener = new ASSEventListener() { + public final SubtitleEventListener getSubEventListener() { return subEventListener; } + private final SubtitleEventListener subEventListener = new SubtitleEventListener() { @Override - public void run(final ASSEventLine e) { - synchronized( assEventLock ) { - assEventQueue.add(e); + public void run(final SubtitleEvent e) { + synchronized( subEventLock ) { + subEventQueue.add(e); if( DEBUG_SUB ) { - System.err.println("MediaButton: GOT #"+assEventQueue.size()+": "+e); + System.err.println("MediaButton: GOT #"+subEventQueue.size()+": "+e); } } } @@ -179,11 +207,18 @@ public class MediaButton extends TexSeqButton { System.err.println("MediaButton AttributesChanges: "+eventMask+", when "+when); System.err.println("MediaButton State: "+mp); } - if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Uninit) ) { + if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Init) ) { clearSubtitleCache(); - } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Init) ) { resetGL = true; + } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Uninit) || + eventMask.isSet(GLMediaPlayer.EventMask.Bit.Play) || + eventMask.isSet(GLMediaPlayer.EventMask.Bit.Seek) || + eventMask.isSet(GLMediaPlayer.EventMask.Bit.SID) ) { clearSubtitleCache(); + markStateDirty(); + } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Pause) ) { + clearSubtitleCacheButLast(); + markStateDirty(); } if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Size) ) { // FIXME: mPlayer.resetGLState(); @@ -209,9 +244,15 @@ public class MediaButton extends TexSeqButton { protected void clearImpl(final GL2ES2 gl, final RegionRenderer renderer) { ((GLMediaPlayer)texSeq).stop(); ((GLMediaPlayer)texSeq).seek(0); + clearSubtitleCache(); } @Override protected void destroyImpl(final GL2ES2 gl, final RegionRenderer renderer) { + ((GLMediaPlayer)texSeq).stop(); + clearSubtitleCache(); + subTexImg.destroy(gl, renderer); + subLabel.destroy(gl, renderer); + subBlend.destroy(gl, renderer); ((GLMediaPlayer)texSeq).destroy(gl); } @@ -228,6 +269,7 @@ public class MediaButton extends TexSeqButton { if( resetGL ) { resetGL = false; try { + clearSubtitleCache(); mPlayer.initGL(gl); if( null != region ) { region.markShapeDirty(); // reset texture data @@ -237,103 +279,210 @@ public class MediaButton extends TexSeqButton { } } super.drawImpl0(gl, renderer, rgba); - if( subEnabled ) { + if( subEnabled && GLMediaPlayer.STREAM_ID_NONE != mPlayer.getSID() ) { drawSubtitle(gl, renderer); } - markStateDirty(); // keep on going + if( GLMediaPlayer.State.Playing == mPlayer.getState() ) { + markStateDirty(); // keep on going + } }; private final void clearSubtitleCache() { - draw_lastASS = null; - synchronized( assEventLock ) { - assEventQueue.clear(); + synchronized( subEventLock ) { + final SubtitleEvent lastSub = drawLastSub; + drawLastSub = null; + if( null != lastSub ) { + lastSub.release(); + } + subTexImg.getImageSequence().removeAllFrames(); + for(final SubtitleEvent e : subEventQueue) { + e.release(); + } + subEventQueue.clear(); + if( DEBUG_SUB ) { + System.err.println("MediaButton.clearSubtitleCache: "+subEventQueue.size()+", last "+lastSub); + } + } + } + private final void clearSubtitleCacheButLast() { + synchronized( subEventLock ) { + final SubtitleEvent lastSub = drawLastSub; + for(int i=subEventQueue.size()-1; i>=0; --i) { + final SubtitleEvent e = subEventQueue.get(i); + if( lastSub != e ) { + e.release(); + subEventQueue.remove(i); + } + } + if( DEBUG_SUB ) { + System.err.println("MediaButton.clearSubtitleCacheButLast: "+subEventQueue.size()+", last "+lastSub); + } } } - private static final int SUB_MIN_DURATION = 5; // min duration 1s, broken ASS have <= 3ms private final void drawSubtitle(final GL2ES2 gl, final RegionRenderer renderer) { final GLMediaPlayer mPlayer = (GLMediaPlayer)texSeq; final int pts = mPlayer.getPTS().getCurrent(); - // Validate draw_lastASS timeout - ASSEventLine lastASS = draw_lastASS; + // Validate draw_lastSub timeout + SubtitleEvent lastSub = drawLastSub; { - if( null != lastASS && lastASS.pts_end < pts && lastASS.getDuration() > SUB_MIN_DURATION ) { + if( null != lastSub && lastSub.pts_end < pts ) { if( DEBUG_SUB ) { - System.err.println("MediaButton: Drop.0: pts "+pts+", "+lastASS); + System.err.println("MediaButton: Drop.0: pts "+pts+", "+lastSub); } - draw_lastASS = null; - lastASS = null; + drawLastSub = null; + lastSub.release(); + lastSub = null; } } - // dequeue and earmark new subtitle in time - final ASSEventLine ass; - final boolean newASS; + // Dequeue and earmark new subtitle in time. + // A new subtitle (empty as well) may simply replace an older one, + // allowing PGS subtitles to work (infinite end-time) + final SubtitleEvent sub; + final boolean newSub; { - final ASSEventLine gotASS; - synchronized( assEventLock ) { - if( assEventQueue.size() > 0 ) { - final ASSEventLine e = assEventQueue.get(0); - if( e.getDuration() <= SUB_MIN_DURATION || ( e.pts_start <= pts && pts <= e.pts_end ) ) { - gotASS = e; - assEventQueue.remove(0); + final SubtitleEvent gotSub; + synchronized( subEventLock ) { + if( subEventQueue.size() > 0 ) { + final SubtitleEvent e = subEventQueue.get(0); + if( e.pts_start <= pts && pts <= e.pts_end ) { + gotSub = e; + subEventQueue.remove(0); } else if( e.pts_end < pts ) { - gotASS = null; - assEventQueue.remove(0); + gotSub = null; + subEventQueue.remove(0); + e.release(); if( DEBUG_SUB ) { System.err.println("MediaButton: Drop.1: pts "+pts+", "+e); } } else { - gotASS = null; + // subtitle for the future, keep it + gotSub = null; } } else { - gotASS = null; + gotSub = null; } } - if( null == gotASS || gotASS == lastASS ) { - ass = lastASS; - newASS = false; + if( null == gotSub ) { + sub = lastSub; + newSub = false; } else { - draw_lastASS = gotASS; - ass = gotASS; - newASS = true; + if( null != lastSub ) { + lastSub.release(); + } + lastSub = null; + if( SubtitleEvent.Type.Empty == gotSub.type ) { + gotSub.release(); + sub = null; + newSub = false; + if( DEBUG_SUB ) { + System.err.println("MediaButton: Empty: pts "+pts+", "+gotSub); + } + } else { + drawLastSub = gotSub; + sub = gotSub; + newSub = true; + } } } // drop or draw (update label for new subtitle) - final boolean drawASS; - if( null == ass ) { - draw_lastASS = null; - drawASS = false; - } else { - drawASS = true; - if( newASS ) { - subLabel.setText(ass.text); + if( null == sub ) { + drawLastSub = null; + } else if( SubtitleEvent.Type.Text == sub.type ) { + final SubTextEvent assSub = (SubTextEvent)sub; + if( newSub ) { + subLabel.setText(assSub.text); final AABBox subBox = subLabel.getBounds(gl.getGLProfile()); - final float subLineHeight = subBox.getHeight() / ass.lines; - final float maxWidth = box.getWidth() * 0.95f; - float scale = ( box.getHeight() * subLineHeightPct ) / subLineHeight; + final float subLineHeight = subBox.getHeight() / assSub.lines; + final float maxWidth = box.getWidth() * ASS_SUB_USED_WIDTH; + final float lineHeight = box.getHeight() * subLineHeightPct; + float scale = lineHeight / subLineHeight; if( scale * subBox.getWidth() > maxWidth ) { scale = maxWidth / subBox.getWidth(); } subLabel.setScale(scale, scale, 1); - final float dx_s = ( box.getWidth() - maxWidth ) * 0.5f; - final float dy_s = subLineHeight * subLineDY * scale; - subLabel.moveTo(dx_s, dy_s, 2*subZOffset); - subBlend.setDimension(box.getWidth(), box.getHeight() * subLineHeightPct * ass.lines, 0f); - subBlend.setPosition(0, dy_s, 1*subZOffset); + + final float labelHeight = lineHeight * assSub.lines; + final float blendHeight = labelHeight + lineHeight * ASS_SUB_BLEND_ADDED_HEIGHT; + final Vec2f v_sz = new Vec2f(mPlayer.getWidth(), mPlayer.getHeight()); + final Vec2f s = new Vec2f( box.getWidth(), box.getHeight() ).div( v_sz ); + final float sxy = Math.min( s.x(), s.y() ); + final Vec2f v_ctr = new Vec2f(v_sz).scale(0.5f); // original video size center + final Vec2f b_ctr = new Vec2f(box.getCenter()).scale(1/sxy); + final float d_bl = ( blendHeight - labelHeight ) * 0.5f; + final Vec2f s_p0 = new Vec2f( mPlayer.getWidth() * ( 1f - ASS_SUB_USED_WIDTH )*0.5f, + ( subLineHeight * subLineDY * scale ) / sxy); + final Vec2f s_p0_s = s_p0.sub( v_ctr ).add(b_ctr).scale( sxy ).add(0, d_bl); + subLabel.moveTo(s_p0_s.x(), s_p0_s.y(), 2*subZOffset); + + subBlend.setDimension(box.getWidth(), blendHeight, 0f); + subBlend.setPosition(0, s_p0_s.y() - d_bl, 1*subZOffset); if( DEBUG_SUB ) { - System.err.println("MediaButton: NEXT pts "+pts+", "+ass); + System.err.println("MediaButton: NEXT pts "+pts+", "+sub); } } - } - if( drawASS ) { subBlend.draw(gl, renderer); final PMVMatrix4f pmv = renderer.getMatrix(); pmv.pushMv(); subLabel.applyMatToMv(pmv); subLabel.draw(gl, renderer); pmv.popMv(); - } + } else if( SubtitleEvent.Type.Bitmap == sub.type ) { + final SubBitmapEvent texSub = (SubBitmapEvent)sub; + if( newSub ) { + if( DEBUG_SUB ) { + System.err.println("MediaButton: NEXT pts "+pts+", "+sub); + } + if( null != texSub.texture ) { + final ImageSequence imgSeq = subTexImg.getImageSequence(); + imgSeq.removeAllFrames(); + imgSeq.addFrame(gl, texSub.texture); + final Vec2f v_sz = new Vec2f(mPlayer.getWidth(), mPlayer.getHeight()); + final Vec2f s = new Vec2f( box.getWidth(), box.getHeight() ).div( v_sz ); + final float sxy = Math.min(s.x(), s.y()); + final Vec2f s_sz_s = new Vec2f(texSub.dimension).scale(sxy); + subTexImg.setSize(s_sz_s.x(), s_sz_s.y()); + + final Vec2f v_ctr; + if( CodecID.HDMV_PGS == sub.codec && mPlayer.getWidth() < 1920 && mPlayer.getHeight() == 1080 ) { + // PGS subtitles < 1920 width, e.g. 4:3 1440 width but 1080p + // usually are positioned to 1920 width screen. FIXME: Elaborate, find metrics + v_ctr = new Vec2f(new Vec2f(1920, 1080)).scale(0.5f); // 1080p center + } else { + v_ctr = new Vec2f(v_sz).scale(0.5f); // original video size center + } + final Vec2f b_ctr = new Vec2f(box.getCenter()).scale(1/sxy); + final Vec2f s_p0 = new Vec2f(texSub.position.x(), + v_sz.y() - texSub.position.y() - texSub.dimension.y() ); // y-flip + texSub.position is top-left + final Vec2f s_p0_s = s_p0.minus( v_ctr ).add( b_ctr ).scale( sxy ); + subTexImg.moveTo(s_p0_s.x(), s_p0_s.y(), 2*subZOffset); + if( DEBUG_PGS_POSSZ ) { + // Keep this to ease later adjustments due to specifications like PGS + final Vec2f b_sz = new Vec2f(box.getWidth(), box.getHeight()); + final float v_ar = v_sz.x()/v_sz.y(); + final float b_ar = b_sz.x()/b_sz.y(); + final float s_ar = s_sz_s.x()/s_sz_s.y(); + final float s_x_centered = ( b_sz.x() - s_sz_s.x() ) * 0.5f; + final Vec2f v_ctr_1080p = new Vec2f(new Vec2f(1920, 1080)).scale(0.5f); // 1080p center + final Vec2f v_ctr_o = new Vec2f(v_sz).scale(0.5f); // original video size center + final Vec2f s_sz = new Vec2f(texSub.dimension); + final Vec2f b_ctr_s = new Vec2f(box.getCenter()); + final Vec2f v_p0_ctr = s_p0.minus(v_ctr); // p0 -> v_sz center + final Vec2f s_p1 = b_ctr.plus(v_p0_ctr); + System.err.println("XX video "+v_sz+" (ar "+v_ar+"), ( v_ctr "+v_ctr_o+", v_ctr_1080p "+v_ctr_1080p+" ) -> v_ctr "+v_ctr); + System.err.println("XX sub s_sz "+s_sz+", s_sz_s "+s_sz_s+" (ar "+s_ar+")"); + System.err.println("XX box "+b_sz+" (ar "+b_ar+"), b_ctr "+b_ctr+", b_ctr_s "+b_ctr_s); + System.err.println("XXX s "+s+" -> "+sxy+": sz "+s_sz_s); + System.err.println("XXX p0 "+s_p0+", v_p0_ctr "+v_p0_ctr+", s_p1 "+s_p1+" -> s_p1_s "+s_p0_s+"; sxs_2 "+s_x_centered); + } + } + } + final PMVMatrix4f pmv = renderer.getMatrix(); + pmv.pushMv(); + subTexImg.applyMatToMv(pmv); + subTexImg.draw(gl, renderer); + pmv.popMv(); + } } - private volatile ASSEventLine draw_lastASS; } diff --git a/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java b/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java index d06020eba..2d2ba07f1 100644 --- a/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java +++ b/src/graphui/classes/com/jogamp/graph/ui/widgets/MediaPlayer.java @@ -27,7 +27,6 @@ */ package com.jogamp.graph.ui.widgets; -import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.concurrent.atomic.AtomicReference; @@ -39,7 +38,6 @@ import com.jogamp.common.util.InterruptSource; import com.jogamp.graph.curve.Region; import com.jogamp.graph.curve.opengl.GLRegion; import com.jogamp.graph.font.Font; -import com.jogamp.graph.font.FontFactory; import com.jogamp.graph.ui.Group; import com.jogamp.graph.ui.Scene; import com.jogamp.graph.ui.Shape; @@ -132,7 +130,7 @@ public class MediaPlayer extends Widget { this.setBorderColor(BorderColor).setBorder(BorderSz); this.setInteractive(true).setFixedARatioResize(true); - mButton = new MediaButton(renderModes, aratio, 1, mPlayer, fontInfo, 0.1f); + mButton = new MediaButton(renderModes, aratio, 1, mPlayer, fontInfo); mButton.setName("mp.mButton").setInteractive(false); mButton.setPerp().setPressedColorMod(1f, 1f, 1f, 0.85f); @@ -159,7 +157,7 @@ public class MediaPlayer extends Widget { subLabel.setPerp().setColor(CtrlCellCol); { - mButton.setVerbose(false).addDefaultEventListener().setTextureLetterbox(letterBox); + mButton.setVerbose(false).addDefaultEventListener().setARatioLetterbox(letterBox, new Vec4f(1, 1, 1, 1)); mPlayer.setAudioVolume( 0f ); mPlayer.addEventListener( new GLMediaEventListener() { @Override @@ -172,15 +170,14 @@ public class MediaPlayer extends Widget { audioLabel.setText("audio\n"+mp.getLang(mp.getAID())); subLabel.setText("sub\n"+mp.getLang(mp.getSID())); ctrlSlider.setMinMax(new Vec2f(0, mp.getDuration()), 0); - if( DEBUG ) { - System.err.println(mp.toString()); - } + System.err.println("Init +"+mp.toString()); + for(final GLMediaPlayer.Chapter c : mp.getChapters()) { if( DEBUG ) { System.err.println(c); } final Shape mark = ctrlSlider.addMark(c.start, new Vec4f(0.9f, 0.9f, 0.9f, 0.5f)); - mark.setToolTip(new TooltipText(c.title+"\n"+PTS.millisToTimeStr(c.start, false), fontInfo, 10)); + mark.setToolTip(new TooltipText(c.title+"\n"+PTS.toTimeStr(c.start, false), fontInfo, 10)); } } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Play) ) { playButton.setToggle(true); @@ -231,11 +228,11 @@ public class MediaPlayer extends Widget { infoGroup.setName("mp.info").setInteractive(false); this.addShape( infoGroup.setVisible(false) ); { - final String text = "88:88 / 88:88 (88 %), playing (8.88x, vol 8.88), A/R 8.88\n"+ + final String text = "88:88 / 88:88:88 (88 %), playing (8.88x, vol 8.88), A/R 8.88, vid 8 (H264), aid 8 (eng, AC3), sid 8 (eng, HDMV_PGS)\n"+ "JogAmp's GraphUI Full-Feature Media Player Widget Rocks!"; final AABBox textBounds = fontInfo.getGlyphBounds(text); final float lineHeight = textBounds.getHeight()/2f; // fontInfo.getLineHeight(); - final float sxy = aratio/(2f*textBounds.getWidth()); // add 100% + final float sxy = aratio/(1.1f*textBounds.getWidth()); // add 10% infoLabel = new Label(renderModes, fontInfo, text); infoLabel.setName("mp.info.label"); infoLabel.setInteractive(false); @@ -273,7 +270,9 @@ public class MediaPlayer extends Widget { public void display(final GLAutoDrawable drawable) { final GLAnimatorControl anim = drawable.getAnimator(); if( ( timeLabel.isVisible() || infoLabel.isVisible() ) && - mPlayer.getState() == GLMediaPlayer.State.Playing && null != anim ) + ( mPlayer.getState() == GLMediaPlayer.State.Playing || + mPlayer.getState() == GLMediaPlayer.State.Paused ) && + null != anim ) { final long t1 = anim.getTotalFPSDuration(); if( t1 - t0 >= 333) { @@ -299,10 +298,14 @@ public class MediaPlayer extends Widget { @Override public void dragged(final RangeSlider w, final float old_val, final float val, final float old_val_pct, final float val_pct) { if( DEBUG ) { - System.err.println("Dragged "+w.getName()+": "+PTS.millisToTimeStr(Math.round(val), true)+"ms, "+(val_pct*100f)+"%"); + System.err.println("Dragged "+w.getName()+": "+PTS.toTimeStr(Math.round(val), true)+"ms, "+(val_pct*100f)+"%"); System.err.println("Slider.D "+ctrlSlider.getDescription()); } - seekPlayer( Math.round( val ) ); + final int currentPTS = mPlayer.getPTS().getCurrent(); + final int nextPTS = Math.round( val ); + if( Math.abs(currentPTS - nextPTS) > 10000 ) { // FIXME: 10s + seekPlayer( nextPTS ); + } } }); this.addShape( ctrlSlider.setVisible(false) ); @@ -375,45 +378,45 @@ public class MediaPlayer extends Widget { } { // 5 final Button button = new Button(renderModes, fontSymbols, - fontSymbols.getUTF16String("replay_5"), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); + fontSymbols.getUTF16String("replay_10"), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); button.setName("rew5"); button.setSpacing(SymSpacing, FixedSymSize).setPerp().setColor(CtrlCellCol); button.onClicked((final Shape s) -> { - mPlayer.seek(mPlayer.getPTS().getCurrent() - 5000); + mPlayer.seek(mPlayer.getPTS().getCurrent() - 10000); }); button.addMouseListener(new Shape.MouseGestureAdapter() { @Override public void mouseWheelMoved(final MouseEvent e) { final int pts0 = mPlayer.getPTS().getCurrent(); - final int pts1 = pts0 + (int)(e.getRotation()[1]*1000f); + final int pts1 = pts0 + (int)(e.getRotation()[1]*10000f); if( DEBUG ) { System.err.println("Seek: "+pts0+" -> "+pts1); } mPlayer.seek(pts1); } } ); ctrlGroup.addShape(button); - button.setToolTip(new TooltipText("Replay-5", fontInfo, toolTipScaleY)); + button.setToolTip(new TooltipText("Replay 10s (+scroll)", fontInfo, toolTipScaleY)); } { // 6 final Button button = new Button(renderModes, fontSymbols, - fontSymbols.getUTF16String("forward_5"), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); + fontSymbols.getUTF16String("forward_10"), CtrlButtonWidth, CtrlButtonHeight, zEpsilon); button.setName("fwd5"); button.setSpacing(SymSpacing, FixedSymSize).setPerp().setColor(CtrlCellCol); button.onClicked((final Shape s) -> { - mPlayer.seek(mPlayer.getPTS().getCurrent() + 5000); + mPlayer.seek(mPlayer.getPTS().getCurrent() + 10000); }); button.addMouseListener(new Shape.MouseGestureAdapter() { @Override public void mouseWheelMoved(final MouseEvent e) { final int pts0 = mPlayer.getPTS().getCurrent(); - final int pts1 = pts0 + (int)(e.getRotation()[1]*1000f); + final int pts1 = pts0 + (int)(e.getRotation()[1]*10000f); if( DEBUG ) { System.err.println("Seek: "+pts0+" -> "+pts1); } mPlayer.seek(pts1); } } ); ctrlGroup.addShape(button); - button.setToolTip(new TooltipText("Forward-5", fontInfo, toolTipScaleY)); + button.setToolTip(new TooltipText("Forward 10s (+scroll)", fontInfo, toolTipScaleY)); } { // 7 final Button button = new Button(renderModes, fontSymbols, @@ -437,7 +440,7 @@ public class MediaPlayer extends Widget { } } ); button.setToggle( !mPlayer.isAudioMuted() ); // on == volume ctrlGroup.addShape(button); - button.setToolTip(new TooltipText("Volume", fontInfo, toolTipScaleY)); + button.setToolTip(new TooltipText("Volume (+scroll)", fontInfo, toolTipScaleY)); } { // 8 audioLabel.onClicked((final Shape s) -> { @@ -621,10 +624,10 @@ public class MediaPlayer extends Widget { } /** - * Sets subtitle parameter - * @param subFont subtitle font - * @param subLineHeightPct one subtitle line height percentage of this shape, default is 0.1f - * @param subLineDY y-axis offset to bottom in line-height, defaults to 1/4 (0.25f) + * Sets text/ASS subtitle parameter + * @param subFont text/ASS subtitle font + * @param subLineHeightPct text/ASS subtitle line height percentage, defaults to {@link MediaButton#DEFAULT_ASS_SUB_HEIGHT} + * @param subLineDY text/ASS y-axis offset to bottom in line-height, defaults to {@link MediaButton#DEFAULT_ASS_SUB_POS} */ public void setSubtitleParams(final Font subFont, final float subLineHeightPct, final float subLineDY) { if( null != mButton ) { @@ -632,9 +635,9 @@ public class MediaPlayer extends Widget { } } /** - * Sets subtitle colors - * @param color color for the text, defaults to RGBA {@code 1, 1, 0, 1} - * @param blend blending alpha (darkness), defaults to 0.2f + * Sets text/ASS subtitle colors + * @param color color for the text/ASS, defaults to {@link MediaButton#DEFAULT_ASS_SUB_COLOR} + * @param blend blending alpha (darkness), defaults to {@link MediaButton#DEFAULT_ASS_SUB_BLEND} */ public void setSubtitleColor(final Vec4f color, final float blend) { if( null != mButton ) { @@ -659,36 +662,35 @@ public class MediaPlayer extends Widget { final float pct = (float)ptsMS / (float)durationMS; if( full ) { final String text1 = String.format("%s / %s (%.0f %%), %s (%01.2fx, vol %1.2f), A/R %.2f, fps %02.1f, kbps %.2f", - PTS.millisToTimeStr(ptsMS, false), PTS.millisToTimeStr(durationMS, false), pct*100, + PTS.toTimeStr(ptsMS, false), PTS.toTimeStr(durationMS, false), pct*100, mPlayer.getState().toString().toLowerCase(), mPlayer.getPlaySpeed(), mPlayer.getAudioVolume(), aspect, mPlayer.getFramerate(), mPlayer.getStreamBitrate()/1000.0f); - final String text2 = String.format("video: id %d (%s), kbps %.2f, codec %s", - mPlayer.getVID(), mPlayer.getLang(mPlayer.getVID()), mPlayer.getVideoBitrate()/1000.0f, mPlayer.getVideoCodec()); - final String text3 = String.format("audio: id %d (%s), kbps %.2f, codec %s; sid %d (%s)", - mPlayer.getAID(), mPlayer.getLang(mPlayer.getAID()), mPlayer.getAudioBitrate()/1000.0f, mPlayer.getAudioCodec(), - mPlayer.getSID(), mPlayer.getLang(mPlayer.getSID()) ); - final String text4 = String.format("sub : id %d (%s), codec %s", - mPlayer.getSID(), mPlayer.getLang(mPlayer.getSID()), mPlayer.getSubtitleCodec()); + final String text2 = String.format("video: id %d (%s), kbps %.2f, codec %s/'%s'", + mPlayer.getVID(), mPlayer.getLang(mPlayer.getVID()), mPlayer.getVideoBitrate()/1000.0f, mPlayer.getVideoCodecID(), mPlayer.getVideoCodec()); + final String text3 = String.format("audio: id %d (%s), kbps %.2f, codec %s/'%s'", + mPlayer.getAID(), mPlayer.getLang(mPlayer.getAID()), mPlayer.getAudioBitrate()/1000.0f, mPlayer.getAudioCodecID(), mPlayer.getAudioCodec()); + final String text4 = String.format("sub : id %d (%s), codec %s/'%s'", + mPlayer.getSID(), mPlayer.getLang(mPlayer.getSID()), mPlayer.getSubtitleCodecID(), mPlayer.getSubtitleCodec()); return text1+"\n"+text2+"\n"+text3+"\n"+text4+"\n"+mPlayer.getTitle()+chapter; } else { final String vinfo, ainfo, sinfo; if( mPlayer.getVID() != GLMediaPlayer.STREAM_ID_NONE ) { - vinfo = String.format((Locale)null, ", vid %d", mPlayer.getVID()); + vinfo = String.format((Locale)null, ", vid %d (%s)", mPlayer.getVID(), mPlayer.getVideoCodecID()); } else { vinfo = ""; } if( mPlayer.getAID() != GLMediaPlayer.STREAM_ID_NONE ) { - ainfo = String.format((Locale)null, ", aid %d (%s)", mPlayer.getAID(), mPlayer.getLang(mPlayer.getAID())); + ainfo = String.format((Locale)null, ", aid %d (%s, %s)", mPlayer.getAID(), mPlayer.getLang(mPlayer.getAID()), mPlayer.getAudioCodecID()); } else { ainfo = ""; } if( mPlayer.getSID() != GLMediaPlayer.STREAM_ID_NONE ) { - sinfo = String.format((Locale)null, ", sid %d (%s)", mPlayer.getSID(), mPlayer.getLang(mPlayer.getSID())); + sinfo = String.format((Locale)null, ", sid %d (%s, %s)", mPlayer.getSID(), mPlayer.getLang(mPlayer.getSID()), mPlayer.getSubtitleCodecID()); } else { sinfo = ""; } final String text1 = String.format("%s / %s (%.0f %%), %s (%01.2fx, vol %1.2f), A/R %.2f%s%s%s", - PTS.millisToTimeStr(ptsMS, false), PTS.millisToTimeStr(durationMS, false), pct*100, + PTS.toTimeStr(ptsMS, false), PTS.toTimeStr(durationMS, false), pct*100, mPlayer.getState().toString().toLowerCase(), mPlayer.getPlaySpeed(), mPlayer.getAudioVolume(), aspect, vinfo, ainfo, sinfo); return text1+"\n"+mPlayer.getTitle()+chapter; } @@ -699,6 +701,6 @@ public class MediaPlayer extends Widget { public static String getMultilineTime(final int ptsMS, final int durationMS) { final float pct = (float)ptsMS / (float)durationMS; return String.format("%.0f %%%n%s%n%s", - pct*100, PTS.millisToTimeStr(ptsMS, false), PTS.millisToTimeStr(durationMS, false)); + pct*100, PTS.toTimeStr(ptsMS, false), PTS.toTimeStr(durationMS, false)); } } |