/** * Copyright 2014-2024 JogAmp Community. All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, are * permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this list of * conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, this list * of conditions and the following disclaimer in the documentation and/or other materials * provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY JogAmp Community ``AS IS'' AND ANY EXPRESS OR IMPLIED * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL JogAmp Community OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * * The views and conclusions contained in the software and documentation are those of the * authors and should not be interpreted as representing official policies, either expressed * or implied, of JogAmp Community. */ package com.jogamp.graph.ui.shapes; import com.jogamp.opengl.GL2ES2; import com.jogamp.opengl.GLProfile; 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.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.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}. *

* GraphUI is GPU based and resolution independent. *

*

* This button is rendered with a round oval shape. * To render it rectangular, {@link #setCorner(float)} to zero. *

*

* Default colors (toggle-on is full color): * - non-toggle: 1 * color * - pressed: 0.9 * color * - toggle-off: 0.8 * color * - toggle-on: 1.0 * color *

*/ public class MediaButton extends TexSeqButton { private final boolean DEBUG_SUB = false; private boolean verbose = false; private final Label subLabel; private final float subZOffset; private boolean subEnabled; private float subLineHeightPct; private final List assEventQueue = new ArrayList(); private final Object assEventLock = new Object(); public MediaButton(final int renderModes, final float width, final float height, final GLMediaPlayer mPlayer) { this(renderModes, width, height, mPlayer, null, 0); } /** * * @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 */ public MediaButton(final int renderModes, final float width, final float height, final GLMediaPlayer mPlayer, final Font subFont, final float subLineHeightPct) { super(renderModes & ~Region.AA_RENDERING_MASK, width, height, mPlayer); setColor(1.0f, 1.0f, 1.0f, 0.0f); setPressedColorMod(0.9f, 0.9f, 0.9f, 0.7f); setToggleOffColorMod(0.8f, 0.8f, 0.8f, 1.0f); setToggleOnColorMod(1.0f, 1.0f, 1.0f, 1.0f); mPlayer.setASSEventListener(assEventListener); final Font f; if( null != subFont ) { f = subFont; subEnabled = true; } else { f = Scene.getDefaultFont(); subEnabled = false; } this.subZOffset = Button.DEFAULT_LABEL_ZOFFSET; this.subLineHeightPct = subLineHeightPct; this.subLabel = new Label(renderModes, f, ""); this.subLabel.setColor( new Vec4f( 1f, 1, 0f, 1.0f ) ); this.subLabel.moveTo(0, 0, subZOffset); } /** * Sets subtitle parameter * @param subFont subtitle font * @param subLineHeightPct one subtitle line height percentage of this shape, default is 0.1f */ public void setSubtitleParams(final Font subFont, final float subLineHeightPct) { this.subLabel.setFont(subFont); this.subLineHeightPct = subLineHeightPct; this.subEnabled = true; } public final ASSEventListener getASSEventListener() { return assEventListener; } private final ASSEventListener assEventListener = new ASSEventListener() { @Override public void run(final ASSEventLine e) { synchronized( assEventLock ) { assEventQueue.add(e); if( DEBUG_SUB ) { System.err.println("MediaButton: GOT #"+assEventQueue.size()+": "+e); } } } }; public MediaButton setVerbose(final boolean v) { verbose = v; return this; } /** * Add the default {@link GLMediaEventListener} to {@link #getGLMediaPlayer() this class's GLMediaPlayer}. */ public MediaButton addDefaultEventListener() { getGLMediaPlayer().addEventListener(defGLMediaEventListener); return this; } public final GLMediaPlayer getGLMediaPlayer() { return (GLMediaPlayer)texSeq; } public final AudioSink getAudioSink() { return getGLMediaPlayer().getAudioSink(); } private final GLMediaEventListener defGLMediaEventListener = new GLMediaEventListener() { @Override public void newFrameAvailable(final GLMediaPlayer ts, final TextureFrame newFrame, final long when) { // texButton.markStateDirty(); } @Override public void attributesChanged(final GLMediaPlayer mp, final GLMediaPlayer.EventMask eventMask, final long when) { if( verbose ) { System.err.println("MediaButton AttributesChanges: "+eventMask+", when "+when); System.err.println("MediaButton State: "+mp); } if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Uninit) ) { clearSubtitleCache(); } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Init) ) { resetGL = true; clearSubtitleCache(); } if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Size) ) { // FIXME: mPlayer.resetGLState(); } if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.EOS) ) { new InterruptSource.Thread() { @Override public void run() { // loop for-ever .. mp.seek(0); mp.resume(); } }.start(); } else if( eventMask.isSet(GLMediaPlayer.EventMask.Bit.Error) ) { final StreamException se = mp.getStreamException(); if( null != se ) { se.printStackTrace(); } } } }; @Override protected void clearImpl(final GL2ES2 gl, final RegionRenderer renderer) { ((GLMediaPlayer)texSeq).stop(); ((GLMediaPlayer)texSeq).seek(0); } @Override protected void destroyImpl(final GL2ES2 gl, final RegionRenderer renderer) { ((GLMediaPlayer)texSeq).destroy(gl); } volatile boolean resetGL = true; @Override protected void addShapeToRegion(final GLProfile glp, final GL2ES2 gl) { super.addShapeToRegion(glp, gl); } @Override protected final void drawImpl0(final GL2ES2 gl, final RegionRenderer renderer, final Vec4f rgba) { final GLMediaPlayer mPlayer = (GLMediaPlayer)texSeq; if( resetGL ) { resetGL = false; try { mPlayer.initGL(gl); if( null != region ) { region.markShapeDirty(); // reset texture data } } catch (final Exception e) { e.printStackTrace(); } } super.drawImpl0(gl, renderer, rgba); if( subEnabled ) { drawSubtitle(gl, renderer); } markStateDirty(); // keep on going }; private final void clearSubtitleCache() { draw_lastASS = null; synchronized( assEventLock ) { assEventQueue.clear(); } } 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().get(Clock.currentMillis()); // Validate draw_lastASS timeout ASSEventLine lastASS = draw_lastASS; { if( null != lastASS && lastASS.pts_end < pts && lastASS.getDuration() > SUB_MIN_DURATION ) { if( DEBUG_SUB ) { System.err.println("MediaButton: Drop.0: pts "+pts+", "+lastASS); } draw_lastASS = null; lastASS = null; } } // dequeue and earmark new subtitle in time final ASSEventLine ass; final boolean newASS; { 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); } else if( e.pts_end < pts ) { gotASS = null; assEventQueue.remove(0); if( DEBUG_SUB ) { System.err.println("MediaButton: Drop.1: pts "+pts+", "+e); } } else { gotASS = null; } } else { gotASS = null; } } if( null == gotASS || gotASS == lastASS ) { ass = lastASS; newASS = false; } else { draw_lastASS = gotASS; ass = gotASS; newASS = 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); final AABBox subBox = subLabel.getBounds(gl.getGLProfile()); final float subLineHeight = subBox.getHeight() / ass.lines; final float maxWidth = this.box.getWidth() * 0.95f; float scale = ( this.box.getHeight() * subLineHeightPct ) / subLineHeight; if( scale * subBox.getWidth() > maxWidth ) { scale = maxWidth / subBox.getWidth(); } subLabel.setScale(scale, scale, 1); final float dx = ( this.box.getWidth() - maxWidth ) * 0.5f; final float dy = subLineHeight * scale * 0.25f; this.subLabel.moveTo(dx, dy, subZOffset); if( DEBUG_SUB ) { System.err.println("MediaButton: NEXT pts "+pts+", "+ass); } } } if( drawASS ) { final PMVMatrix4f pmv = renderer.getMatrix(); pmv.pushMv(); subLabel.applyMatToMv(pmv); subLabel.draw(gl, renderer); pmv.popMv(); } } private volatile ASSEventLine draw_lastASS; }