diff --git a/src/org/thoughtcrime/securesms/jobs/MediaResizer.java b/src/org/thoughtcrime/securesms/jobs/MediaResizer.java index 860187ed1b..a9310ef330 100644 --- a/src/org/thoughtcrime/securesms/jobs/MediaResizer.java +++ b/src/org/thoughtcrime/securesms/jobs/MediaResizer.java @@ -22,7 +22,7 @@ import org.thoughtcrime.securesms.util.BitmapDecodingException; import org.thoughtcrime.securesms.util.BitmapUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.video.InMemoryTranscoder; -import org.thoughtcrime.securesms.video.videoconverter.BadVideoException; +import org.thoughtcrime.securesms.video.videoconverter.EncodingException; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -114,7 +114,7 @@ final class MediaResizer { } } } - } catch (IOException | MmsException | BadVideoException e) { + } catch (IOException | MmsException | EncodingException e) { throw new UndeliverableMessageException("Failed to transcode", e); } } diff --git a/src/org/thoughtcrime/securesms/video/InMemoryTranscoder.java b/src/org/thoughtcrime/securesms/video/InMemoryTranscoder.java index 4863e6ffe2..5eb42444b6 100644 --- a/src/org/thoughtcrime/securesms/video/InMemoryTranscoder.java +++ b/src/org/thoughtcrime/securesms/video/InMemoryTranscoder.java @@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.MediaStream; import org.thoughtcrime.securesms.transport.UndeliverableMessageException; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; -import org.thoughtcrime.securesms.video.videoconverter.BadVideoException; +import org.thoughtcrime.securesms.video.videoconverter.EncodingException; import org.thoughtcrime.securesms.video.videoconverter.MediaConverter; import java.io.Closeable; @@ -80,7 +80,7 @@ public final class InMemoryTranscoder implements Closeable { : OUTPUT_FORMAT; } - public @NonNull MediaStream transcode(@NonNull Progress progress) throws IOException, UndeliverableMessageException, BadVideoException { + public @NonNull MediaStream transcode(@NonNull Progress progress) throws IOException, UndeliverableMessageException, EncodingException { if (memoryFile != null) throw new AssertionError("Not expecting to reuse transcoder"); float durationSec = duration / 1000f; diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/BadVideoException.java b/src/org/thoughtcrime/securesms/video/videoconverter/BadVideoException.java deleted file mode 100644 index 83bbed6d89..0000000000 --- a/src/org/thoughtcrime/securesms/video/videoconverter/BadVideoException.java +++ /dev/null @@ -1,6 +0,0 @@ -package org.thoughtcrime.securesms.video.videoconverter; - -public final class BadVideoException extends Exception { - BadVideoException() { - } -} diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/EncodingException.java b/src/org/thoughtcrime/securesms/video/videoconverter/EncodingException.java new file mode 100644 index 0000000000..5a3011796a --- /dev/null +++ b/src/org/thoughtcrime/securesms/video/videoconverter/EncodingException.java @@ -0,0 +1,11 @@ +package org.thoughtcrime.securesms.video.videoconverter; + +public final class EncodingException extends Exception { + EncodingException(String message) { + super(message); + } + + EncodingException(String message, Exception inner) { + super(message, inner); + } +} diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/InputSurface.java b/src/org/thoughtcrime/securesms/video/videoconverter/InputSurface.java index 41c973de2a..ffa4e233cd 100644 --- a/src/org/thoughtcrime/securesms/video/videoconverter/InputSurface.java +++ b/src/org/thoughtcrime/securesms/video/videoconverter/InputSurface.java @@ -52,7 +52,7 @@ final class InputSurface { /** * Creates an InputSurface from a Surface. */ - public InputSurface(Surface surface) { + InputSurface(Surface surface) throws TranscodingException { if (surface == null) { throw new NullPointerException(); } @@ -64,15 +64,15 @@ final class InputSurface { /** * Prepares EGL. We want a GLES 2.0 context and a surface that supports recording. */ - private void eglSetup() { + private void eglSetup() throws TranscodingException { mEGLDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mEGLDisplay == EGL14.EGL_NO_DISPLAY) { - throw new RuntimeException("unable to get EGL14 display"); + throw new TranscodingException("unable to get EGL14 display"); } int[] version = new int[2]; if (!EGL14.eglInitialize(mEGLDisplay, version, 0, version, 1)) { mEGLDisplay = null; - throw new RuntimeException("unable to initialize EGL14"); + throw new TranscodingException("unable to initialize EGL14"); } // Configure EGL for pbuffer and OpenGL ES 2.0. We want enough RGB bits @@ -89,7 +89,7 @@ final class InputSurface { int[] numConfigs = new int[1]; if (!EGL14.eglChooseConfig(mEGLDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0)) { - throw new RuntimeException("unable to find RGB888+recordable ES2 EGL config"); + throw new TranscodingException("unable to find RGB888+recordable ES2 EGL config"); } // Configure context for OpenGL ES 2.0. @@ -101,7 +101,7 @@ final class InputSurface { attrib_list, 0); checkEglError("eglCreateContext"); if (mEGLContext == null) { - throw new RuntimeException("null context"); + throw new TranscodingException("null context"); } // Create a window surface, and attach it to the Surface we received. @@ -112,7 +112,7 @@ final class InputSurface { surfaceAttribs, 0); checkEglError("eglCreateWindowSurface"); if (mEGLSurface == null) { - throw new RuntimeException("surface was null"); + throw new TranscodingException("surface was null"); } } @@ -143,16 +143,16 @@ final class InputSurface { /** * Makes our EGL context and surface current. */ - public void makeCurrent() { + void makeCurrent() throws TranscodingException { if (!EGL14.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { - throw new RuntimeException("eglMakeCurrent failed"); + throw new TranscodingException("eglMakeCurrent failed"); } } /** * Calls eglSwapBuffers. Use this to "publish" the current frame. */ - public boolean swapBuffers() { + boolean swapBuffers() { return EGL14.eglSwapBuffers(mEGLDisplay, mEGLSurface); } @@ -166,14 +166,14 @@ final class InputSurface { /** * Sends the presentation time stamp to EGL. Time is expressed in nanoseconds. */ - public void setPresentationTime(long nsecs) { + void setPresentationTime(long nsecs) { EGLExt.eglPresentationTimeANDROID(mEGLDisplay, mEGLSurface, nsecs); } /** * Checks for EGL errors. */ - private void checkEglError(String msg) { + private static void checkEglError(String msg) throws TranscodingException { boolean failed = false; int error; while ((error = EGL14.eglGetError()) != EGL14.EGL_SUCCESS) { @@ -181,7 +181,7 @@ final class InputSurface { failed = true; } if (failed) { - throw new RuntimeException("EGL error encountered (see log)"); + throw new TranscodingException("EGL error encountered (see log)"); } } } diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java b/src/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java index 89b344d07b..e6640e9825 100644 --- a/src/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java +++ b/src/org/thoughtcrime/securesms/video/videoconverter/MediaConverter.java @@ -139,7 +139,7 @@ public final class MediaConverter { } @WorkerThread - public void convert() throws BadVideoException, IOException { + public void convert() throws EncodingException, IOException { // Exception that may be thrown during release. Exception exception = null; Muxer muxer = null; @@ -151,8 +151,7 @@ public final class MediaConverter { audioTrackConverter = AudioTrackConverter.create(mInput, mTimeFrom, mTimeTo, mAudioBitrate); if (videoTrackConverter == null && audioTrackConverter == null) { - Log.e(TAG, "no video and audio tracks"); - throw new BadVideoException(); + throw new EncodingException("No video and audio tracks"); } muxer = mOutput.createMuxer(); @@ -162,7 +161,7 @@ public final class MediaConverter { audioTrackConverter, muxer); - } catch (BadVideoException | IOException e) { + } catch (EncodingException | IOException e) { Log.e(TAG, "error converting", e); exception = e; throw e; @@ -207,7 +206,7 @@ public final class MediaConverter { } } if (exception != null) { - throw new RuntimeException(exception); + throw new EncodingException("Transcode failed", exception); } } @@ -217,7 +216,7 @@ public final class MediaConverter { private void doExtractDecodeEditEncodeMux( final @Nullable VideoTrackConverter videoTrackConverter, final @Nullable AudioTrackConverter audioTrackConverter, - final @NonNull Muxer muxer) throws IOException { + final @NonNull Muxer muxer) throws IOException, TranscodingException { boolean muxing = false; int percentProcessed = 0; diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/OutputSurface.java b/src/org/thoughtcrime/securesms/video/videoconverter/OutputSurface.java index 5c279e6dbb..53f329eb91 100644 --- a/src/org/thoughtcrime/securesms/video/videoconverter/OutputSurface.java +++ b/src/org/thoughtcrime/securesms/video/videoconverter/OutputSurface.java @@ -69,7 +69,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { * EGL context and surface will be made current. Creates a Surface that can be passed * to MediaCodec.configure(). */ - public OutputSurface(int width, int height) { + OutputSurface(int width, int height) throws TranscodingException { if (width <= 0 || height <= 0) { throw new IllegalArgumentException(); } @@ -84,7 +84,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { * Creates an OutputSurface using the current EGL context. Creates a Surface that can be * passed to MediaCodec.configure(). */ - public OutputSurface() { + OutputSurface() throws TranscodingException { setup(); } @@ -92,7 +92,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { * Creates instances of TextureRender and SurfaceTexture, and a Surface associated * with the SurfaceTexture. */ - private void setup() { + private void setup() throws TranscodingException { mTextureRender = new TextureRender(); mTextureRender.surfaceCreated(); @@ -122,11 +122,11 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { /** * Prepares EGL. We want a GLES 2.0 context and a surface that supports pbuffer. */ - private void eglSetup(int width, int height) { + private void eglSetup(int width, int height) throws TranscodingException { mEGL = (EGL10)EGLContext.getEGL(); mEGLDisplay = mEGL.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); if (!mEGL.eglInitialize(mEGLDisplay, null)) { - throw new RuntimeException("unable to initialize EGL10"); + throw new TranscodingException("unable to initialize EGL10"); } // Configure EGL for pbuffer and OpenGL ES 2.0. We want enough RGB bits @@ -142,7 +142,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { EGLConfig[] configs = new EGLConfig[1]; int[] numConfigs = new int[1]; if (!mEGL.eglChooseConfig(mEGLDisplay, attribList, configs, 1, numConfigs)) { - throw new RuntimeException("unable to find RGB888+pbuffer EGL config"); + throw new TranscodingException("unable to find RGB888+pbuffer EGL config"); } // Configure context for OpenGL ES 2.0. @@ -154,7 +154,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { attrib_list); checkEglError("eglCreateContext"); if (mEGLContext == null) { - throw new RuntimeException("null context"); + throw new TranscodingException("null context"); } // Create a pbuffer surface. By using this for output, we can use glReadPixels @@ -167,7 +167,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { mEGLSurface = mEGL.eglCreatePbufferSurface(mEGLDisplay, configs[0], surfaceAttribs); checkEglError("eglCreatePbufferSurface"); if (mEGLSurface == null) { - throw new RuntimeException("surface was null"); + throw new TranscodingException("surface was null"); } } @@ -206,13 +206,13 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { /** * Makes our EGL context and surface current. */ - public void makeCurrent() { + private void makeCurrent() throws TranscodingException { if (mEGL == null) { - throw new RuntimeException("not configured for makeCurrent"); + throw new TranscodingException("not configured for makeCurrent"); } checkEglError("before makeCurrent"); if (!mEGL.eglMakeCurrent(mEGLDisplay, mEGLSurface, mEGLSurface, mEGLContext)) { - throw new RuntimeException("eglMakeCurrent failed"); + throw new TranscodingException("eglMakeCurrent failed"); } } @@ -226,7 +226,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { /** * Replaces the fragment shader. */ - public void changeFragmentShader(String fragmentShader) { + void changeFragmentShader(String fragmentShader) throws TranscodingException { mTextureRender.changeFragmentShader(fragmentShader); } @@ -235,36 +235,38 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { * the OutputSurface object, after the onFrameAvailable callback has signaled that new * data is available. */ - public void awaitNewImage() { - final int TIMEOUT_MS = 500; + void awaitNewImage() throws TranscodingException { + final int TIMEOUT_MS = 750; synchronized (mFrameSyncObject) { + final long expireTime = System.currentTimeMillis() + TIMEOUT_MS; + while (!mFrameAvailable) { try { // Wait for onFrameAvailable() to signal us. Use a timeout to avoid // stalling the test if it doesn't arrive. mFrameSyncObject.wait(TIMEOUT_MS); - if (!mFrameAvailable) { - // TODO: if "spurious wakeup", continue while loop - throw new RuntimeException("Surface frame wait timed out"); + + if (!mFrameAvailable && System.currentTimeMillis() > expireTime) { + throw new TranscodingException("Surface frame wait timed out"); } } catch (InterruptedException ie) { // shouldn't happen - throw new RuntimeException(ie); + throw new TranscodingException(ie); } } mFrameAvailable = false; } // Latch the data. - mTextureRender.checkGlError("before updateTexImage"); + TextureRender.checkGlError("before updateTexImage"); mSurfaceTexture.updateTexImage(); } /** * Draws the data from SurfaceTexture onto the current EGL surface. */ - public void drawImage() { + void drawImage() throws TranscodingException { mTextureRender.drawFrame(mSurfaceTexture); } @@ -273,7 +275,11 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { if (VERBOSE) Log.d(TAG, "new frame available"); synchronized (mFrameSyncObject) { if (mFrameAvailable) { - throw new RuntimeException("mFrameAvailable already set, frame could be dropped"); + try { + throw new TranscodingException("mFrameAvailable already set, frame could be dropped"); + } catch (TranscodingException e) { + e.printStackTrace(); + } } mFrameAvailable = true; mFrameSyncObject.notifyAll(); @@ -283,7 +289,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { /** * Checks for EGL errors. */ - private void checkEglError(String msg) { + private void checkEglError(String msg) throws TranscodingException { boolean failed = false; int error; while ((error = mEGL.eglGetError()) != EGL10.EGL_SUCCESS) { @@ -291,7 +297,7 @@ final class OutputSurface implements SurfaceTexture.OnFrameAvailableListener { failed = true; } if (failed) { - throw new RuntimeException("EGL error encountered (see log)"); + throw new TranscodingException("EGL error encountered (see log)"); } } } diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/TextureRender.java b/src/org/thoughtcrime/securesms/video/videoconverter/TextureRender.java index 5950f71a83..1de9b2b1b9 100644 --- a/src/org/thoughtcrime/securesms/video/videoconverter/TextureRender.java +++ b/src/org/thoughtcrime/securesms/video/videoconverter/TextureRender.java @@ -79,7 +79,7 @@ final class TextureRender { private int maPositionHandle; private int maTextureHandle; - public TextureRender() { + TextureRender() { mTriangleVertices = ByteBuffer.allocateDirect( mTriangleVerticesData.length * FLOAT_SIZE_BYTES) .order(ByteOrder.nativeOrder()).asFloatBuffer(); @@ -88,11 +88,11 @@ final class TextureRender { Matrix.setIdentityM(mSTMatrix, 0); } - public int getTextureId() { + int getTextureId() { return mTextureID; } - public void drawFrame(SurfaceTexture st) { + void drawFrame(SurfaceTexture st) throws TranscodingException { checkGlError("onDrawFrame start"); st.getTransformMatrix(mSTMatrix); @@ -131,32 +131,32 @@ final class TextureRender { /** * Initializes GL state. Call this after the EGL surface has been created and made current. */ - public void surfaceCreated() { + void surfaceCreated() throws TranscodingException { mProgram = createProgram(VERTEX_SHADER, FRAGMENT_SHADER); if (mProgram == 0) { - throw new RuntimeException("failed creating program"); + throw new TranscodingException("failed creating program"); } maPositionHandle = GLES20.glGetAttribLocation(mProgram, "aPosition"); checkGlError("glGetAttribLocation aPosition"); if (maPositionHandle == -1) { - throw new RuntimeException("Could not get attrib location for aPosition"); + throw new TranscodingException("Could not get attrib location for aPosition"); } maTextureHandle = GLES20.glGetAttribLocation(mProgram, "aTextureCoord"); checkGlError("glGetAttribLocation aTextureCoord"); if (maTextureHandle == -1) { - throw new RuntimeException("Could not get attrib location for aTextureCoord"); + throw new TranscodingException("Could not get attrib location for aTextureCoord"); } muMVPMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uMVPMatrix"); checkGlError("glGetUniformLocation uMVPMatrix"); if (muMVPMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uMVPMatrix"); + throw new TranscodingException("Could not get attrib location for uMVPMatrix"); } muSTMatrixHandle = GLES20.glGetUniformLocation(mProgram, "uSTMatrix"); checkGlError("glGetUniformLocation uSTMatrix"); if (muSTMatrixHandle == -1) { - throw new RuntimeException("Could not get attrib location for uSTMatrix"); + throw new TranscodingException("Could not get attrib location for uSTMatrix"); } int[] textures = new int[1]; @@ -180,15 +180,15 @@ final class TextureRender { /** * Replaces the fragment shader. */ - public void changeFragmentShader(String fragmentShader) { + public void changeFragmentShader(String fragmentShader) throws TranscodingException { GLES20.glDeleteProgram(mProgram); mProgram = createProgram(VERTEX_SHADER, fragmentShader); if (mProgram == 0) { - throw new RuntimeException("failed creating program"); + throw new TranscodingException("failed creating program"); } } - private int loadShader(int shaderType, String source) { + private static int loadShader(int shaderType, String source) throws TranscodingException { int shader = GLES20.glCreateShader(shaderType); checkGlError("glCreateShader type=" + shaderType); GLES20.glShaderSource(shader, source); @@ -204,7 +204,7 @@ final class TextureRender { return shader; } - private int createProgram(String vertexSource, String fragmentSource) { + private int createProgram(String vertexSource, String fragmentSource) throws TranscodingException { int vertexShader = loadShader(GLES20.GL_VERTEX_SHADER, vertexSource); if (vertexShader == 0) { return 0; @@ -235,11 +235,15 @@ final class TextureRender { return program; } - public void checkGlError(String op) { + static void checkGlError(String msg) throws TranscodingException { + boolean failed = false; int error; while ((error = GLES20.glGetError()) != GLES20.GL_NO_ERROR) { - Log.e(TAG, op + ": glError " + error); - throw new RuntimeException(op + ": glError " + error); + Log.e(TAG, msg + ": GLES20 error: 0x" + Integer.toHexString(error)); + failed = true; + } + if (failed) { + throw new TranscodingException("GLES20 error encountered (see log)"); } } } diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/TranscodingException.java b/src/org/thoughtcrime/securesms/video/videoconverter/TranscodingException.java new file mode 100644 index 0000000000..eb9d4c133c --- /dev/null +++ b/src/org/thoughtcrime/securesms/video/videoconverter/TranscodingException.java @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.video.videoconverter; + +final class TranscodingException extends Exception { + + TranscodingException(String message) { + super(message); + } + + TranscodingException(Throwable inner) { + super(inner); + } +} diff --git a/src/org/thoughtcrime/securesms/video/videoconverter/VideoTrackConverter.java b/src/org/thoughtcrime/securesms/video/videoconverter/VideoTrackConverter.java index 99ab762aef..457c3ef4c0 100644 --- a/src/org/thoughtcrime/securesms/video/videoconverter/VideoTrackConverter.java +++ b/src/org/thoughtcrime/securesms/video/videoconverter/VideoTrackConverter.java @@ -67,7 +67,7 @@ final class VideoTrackConverter { final long timeTo, final int videoResolution, final int videoBitrate, - final @NonNull String videoCodec) throws IOException { + final @NonNull String videoCodec) throws IOException, TranscodingException { final MediaExtractor videoExtractor = input.createExtractor(); final int videoInputTrack = getAndSelectVideoTrackIndex(videoExtractor); @@ -85,7 +85,7 @@ final class VideoTrackConverter { final long timeTo, final int videoResolution, final int videoBitrate, - final @NonNull String videoCodec) throws IOException { + final @NonNull String videoCodec) throws IOException, TranscodingException { mTimeFrom = timeFrom; mTimeTo = timeTo; @@ -175,7 +175,7 @@ final class VideoTrackConverter { } } - void step() throws IOException { + void step() throws IOException, TranscodingException { // Extract video from file and feed to decoder. // Do not extract video if we have determined the output format but we are not yet // ready to mux the frames.