com.simas.vc.background_tasks.FFmpegService.java Source code

Java tutorial

Introduction

Here is the source code for com.simas.vc.background_tasks.FFmpegService.java

Source

/*
 * Copyright (c) 2015. Simas Abramovas
 *
 * This file is part of VideoClipper.
 *
 * VideoClipper is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * VideoClipper is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with VideoClipper. If not, see <http://www.gnu.org/licenses/>.
 */
package com.simas.vc.background_tasks;

import android.app.ActivityManager;
import android.app.IntentService;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.PowerManager;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import android.widget.Toast;
import com.simas.vc.helpers.Utils;
import com.simas.vc.VC;
import com.simas.vc.R;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;

/**
 * Connects to FFmpeg library via JNI inside of a separate process.
 */
public class FFmpegService extends IntentService {

    private static final String TAG = "FFmpegService";
    private static final int INITIAL_ID = 100000;
    private static int sTaskCount = INITIAL_ID;
    private static int sIntentQueueSize;
    private static final NotificationManager NOTIFICATION_MANAGER = (NotificationManager) VC.getAppContext()
            .getSystemService(Context.NOTIFICATION_SERVICE);
    /**
     * Used to keep the CPU working while the screen is locked.
     */
    private static final String WAKE_LOCK_TAG = "FFmpeg";
    private final PowerManager.WakeLock mWakeLock = ((PowerManager) VC.getAppContext()
            .getSystemService(POWER_SERVICE)).newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG);

    // Argument keys
    public static final String ARG_EXEC_ARGS = "argc_n_argv";
    public static final String ARG_OUTPUT_FILE = "output_file";
    public static final String ARG_PROGRESS_FILE = "progress_file";
    public static final String ARG_OUTPUT_DURATION = "output_length";

    public FFmpegService() {
        super(TAG);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        // Show notification
        String[] args = intent.getStringArrayExtra(ARG_EXEC_ARGS);
        File output = (File) intent.getSerializableExtra(ARG_OUTPUT_FILE);
        File progress = (File) intent.getSerializableExtra(ARG_PROGRESS_FILE);
        int duration = intent.getIntExtra(ARG_OUTPUT_DURATION, 0);

        // Launch progress notifier
        final ProgressNotifier notifier = new ProgressNotifier(duration, output, progress);
        Utils.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                // AsyncTasks should be executed on the UI thread
                notifier.execute();
            }
        });
        // Launch the process itself
        final int ffmpegResult = FFmpeg.cFFmpeg(args);
        // Notifier cancelling must be run on the UI thread too, so it doesn't overlap the execution
        Utils.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                notifier.ffmpegCancel(ffmpegResult);
            }
        });
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        // Enable more releases than acquirements
        mWakeLock.setReferenceCounted(false);
        // Start the lock
        mWakeLock.acquire();

        // Increment queue size
        ++sIntentQueueSize;
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Make sure the WakeLock is released
        mWakeLock.release();
    }

    private boolean isRunning() {
        ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
        for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
            if (getClass().getName().equals(service.service.getClassName())) {
                return true;
            }
        }
        return false;
    }

    private class ProgressNotifier extends AsyncTask<Void, Void, Boolean> {

        /**
         * Amount of milliseconds to wait before re-checking the progress file
         */
        private static final int PROGRESS_FILE_WAIT_DURATION = 1000;
        private static final String PROGRESS_KEY = "progress=";
        private static final String OUT_TIME_KEY = "out_time=";
        private static final String END_VALUE = "end";
        private static final String CONTINUE_VALUE = "continue";
        private static final int MAX_FRUITLESS_ITERATIONS = 5;

        private final int mDuration;
        private final String mDurationTime;
        private boolean mFfmpegSucceeded;
        private int mFfmpegReturnCode;
        private File mProgressLog;
        private File mOutput;
        private BufferedReader mReader;
        private NotificationCompat.Builder mBuilder;
        /**
         * Intent used for broadcasting update messages. These are received by {@code
         * ProgressActivity}
         */
        private final Intent mUpdateIntent;
        /**
         * Intent used to open up a {@code ProgressActivity}.
         */
        private final Intent mDisplayIntent;
        private int mFruitlessIterations = 0;

        public ProgressNotifier(int outputDuration, File outputFile, File progressFile) {
            mDuration = outputDuration;
            mDurationTime = Utils.secsToTime(mDuration);
            mOutput = outputFile;
            mProgressLog = progressFile;
            ++sTaskCount;
            mUpdateIntent = new Intent();
            mUpdateIntent.setAction(ProgressActivity.ACTION_DIALOG_UPDATE);
            mUpdateIntent.putExtra(ProgressActivity.ARG_TYPE, ProgressActivity.Type.PROGRESS);

            mDisplayIntent = new Intent(VC.getAppContext(), ProgressActivity.class);
            // Remove any existing progress activities, so onCreate is called instead of onNewIntent
            mDisplayIntent.setAction(ProgressActivity.ACTION_DIALOG_UPDATE);
            mDisplayIntent.putExtra(ProgressActivity.ARG_OUTPUT_FILE, outputFile);
            mDisplayIntent.putExtra(ProgressActivity.ARG_TOTAL_DURATION, mDurationTime);
            mDisplayIntent.putExtra(ProgressActivity.ARG_TYPE, ProgressActivity.Type.PROGRESS);
        }

        @Override
        protected void onPreExecute() {
            super.onPreExecute();

            // Open progress dialog on click
            PendingIntent pendingIntent = PendingIntent.getActivity(VC.getAppContext(), 0, mDisplayIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT);

            mBuilder = new NotificationCompat.Builder(getApplicationContext());
            mBuilder.setContentTitle(Utils.getString(R.string.vc_working))
                    .setTicker(Utils.getString(R.string.initialising))
                    .setContentText(Utils.getString(R.string.initialising)).setSmallIcon(R.drawable.ic_action_merge)
                    .setContentIntent(pendingIntent).setPriority(NotificationCompat.PRIORITY_MAX);

            // If length is not set, show an indeterminate progress notification
            if (mDuration < 1) {
                mBuilder.setProgress(mDuration, 0, true);
            }

            // Create an un-removable notification to display progress
            startForeground(INITIAL_ID, mBuilder.build());
        }

        @Override
        protected Boolean doInBackground(Void... params) {
            for (;;) {
                try {
                    if (isCancelled()) {
                        // On older APIs the return value, when cancelled, is NOT ignored
                        return mFfmpegSucceeded;
                    }
                    mReader = new BufferedReader(new FileReader(mProgressLog));

                    StringBuilder sb = new StringBuilder();
                    String line, lastLine = null;
                    while ((line = mReader.readLine()) != null) {
                        sb.append(line);
                        lastLine = line;
                        sb.append("\n");
                    }

                    // Full content/progress file
                    String pg = sb.toString();

                    // Make sure last line starts with the preferred key
                    if (lastLine != null && lastLine.startsWith(PROGRESS_KEY)) {
                        // Fruitful iteration
                        mFruitlessIterations = 0;

                        // Remove the last line
                        int progressStart = pg.lastIndexOf(PROGRESS_KEY);
                        pg = pg.substring(0, progressStart);

                        // Trim pg to contain the last block
                        int penultimateProgressStart = pg.lastIndexOf(PROGRESS_KEY);
                        if (penultimateProgressStart != -1) {
                            int penultimateProgressEnd = pg.indexOf('\n', penultimateProgressStart);
                            if (penultimateProgressEnd != -1) {
                                pg = pg.substring(penultimateProgressEnd, pg.length());
                            }
                        }

                        // Check if end was reached
                        String progress = lastLine.replaceAll(PROGRESS_KEY, "");
                        if (progress.equals(END_VALUE)) {
                            return true;
                        } else if (mDuration > 0) { // If notification progress is not indeterminate
                            // Calculate outTime start and end indexes
                            int startIndex = pg.lastIndexOf(OUT_TIME_KEY);
                            int endIndex = pg.indexOf("\n", startIndex);
                            int secs = -1;
                            if (startIndex != -1 && endIndex != -1) {
                                // Parse outTime
                                String outTime = pg.substring(startIndex, endIndex);
                                outTime = outTime.replaceAll(OUT_TIME_KEY, "");
                                secs = timeToSecs(outTime);
                            }

                            String curDur = null;
                            if (secs == -1) {
                                // Use an indeterminate progress instead
                                mBuilder.setProgress(mDuration, 0, true);
                                mBuilder.setContentText(Utils.getString(R.string.clipping));
                            } else {
                                mBuilder.setProgress(mDuration, secs, false);

                                // max(currentDuration, totalDuration)
                                if (secs >= mDuration) {
                                    curDur = mDurationTime;
                                } else {
                                    curDur = Utils.secsToTime(secs);
                                }

                                mBuilder.setContentText(String.format("%s %s %s", curDur,
                                        Utils.getString(R.string.out_of), mDurationTime));
                            }
                            // Update display intent
                            mDisplayIntent.putExtra(ProgressActivity.ARG_CUR_DURATION, curDur);
                            mDisplayIntent.putExtra(ProgressActivity.ARG_CONTENT, pg);
                            PendingIntent pIntent = PendingIntent.getActivity(VC.getAppContext(), 0, mDisplayIntent,
                                    PendingIntent.FLAG_CANCEL_CURRENT);
                            mBuilder.setContentIntent(pIntent);

                            // Update notification text (initially it's "initialising")
                            mBuilder.setTicker(Utils.getString(R.string.clipping));

                            // Update notification
                            NOTIFICATION_MANAGER.notify(INITIAL_ID, mBuilder.build());

                            // Send a broadcast message about the values update
                            mUpdateIntent.putExtra(ProgressActivity.ARG_CONTENT, pg);
                            mUpdateIntent.putExtra(ProgressActivity.ARG_CUR_DURATION, curDur);
                            sendBroadcast(mUpdateIntent);
                        }
                    } else {
                        if (++mFruitlessIterations >= MAX_FRUITLESS_ITERATIONS) {
                            // Stop the service if max iterations reached without getting any data
                            return false;
                        }
                    }
                    Thread.sleep(PROGRESS_FILE_WAIT_DURATION);
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (InterruptedException ignored) {
                    Log.i(TAG, "Waiting has been interrupted... Probably by cancelled the task.");
                } finally {
                    try {
                        if (mReader != null)
                            mReader.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

        /**
         * Cancel called after FFmpeg finishes.
         */
        public void ffmpegCancel(int succeeded) {
            mFfmpegReturnCode = succeeded;
            mFfmpegSucceeded = (mFfmpegReturnCode == 0);
            if (cancel(true)) {
                Log.i(TAG, String.format("FFmpeg %s first, so it canceled the notifier.",
                        (mFfmpegSucceeded) ? "succeeded" : "failed"));
            }
        }

        @Override
        protected void onCancelled() {
            super.onCancelled();
            Log.i(TAG,
                    String.format("Service cancelled. FFmpeg has %s", (mFfmpegSucceeded) ? "succeeded" : "failed"));
            finish();
        }

        @Override
        protected void onPostExecute(Boolean didFinish) {
            super.onPostExecute(didFinish);
            Log.i(TAG, String.format("Service finished because it %s.",
                    (didFinish == null || !didFinish) ? "failed" : "succeeded"));
            mFfmpegSucceeded = !(didFinish == null || !didFinish);
            finish();
        }

        private void finish() {
            if (mFfmpegSucceeded) {
                showSuccessNotification();
            } else {
                showFailureNotification();
            }

            // Delete progress file
            mProgressLog.delete();

            // Cancel progress notification if the queue is empty
            if (--sIntentQueueSize <= 0) {
                sIntentQueueSize = 0;

                // Queue's done, release the WakeLock
                mWakeLock.release();

                // stopForeground will remove the notification with INITIAL_ID
                stopForeground(true);
            }
        }

        private void showFailureNotification() {
            // Open error dialog on click
            mDisplayIntent.putExtra(ProgressActivity.ARG_CONTENT, String.valueOf(mFfmpegReturnCode));
            mDisplayIntent.putExtra(ProgressActivity.ARG_TYPE, ProgressActivity.Type.ERROR);
            PendingIntent pendingIntent = PendingIntent.getActivity(VC.getAppContext(), 0, mDisplayIntent,
                    PendingIntent.FLAG_CANCEL_CURRENT);

            // Show notification and a toast only if the progress for the current file isn't shown
            File progressingFile = ProgressActivity.getProgressingFile();
            if (progressingFile == null || progressingFile.compareTo(mOutput) != 0) {
                String str = String.format("android.resource://%s/%s", getPackageName(), R.raw.fail);
                mBuilder = new NotificationCompat.Builder(getApplicationContext());
                mBuilder.setContentTitle(Utils.getString(R.string.vc_failed))
                        .setTicker(Utils.getString(R.string.vc_failed))
                        .setContentText(Utils.getString(R.string.format_clipping_failed, mOutput.getName()))
                        .setSmallIcon(R.drawable.ic_action_error).setContentIntent(pendingIntent)
                        .setAutoCancel(true).setSound(Uri.parse(str));

                // Show the final notification
                NOTIFICATION_MANAGER.notify(sTaskCount, mBuilder.build());

                // Show a toast
                Toast.makeText(getApplicationContext(), R.string.clipping_failed_see_notification,
                        Toast.LENGTH_LONG).show();
            }

            // Send a broadcast message about the occured error
            mUpdateIntent.putExtra(ProgressActivity.ARG_TYPE, ProgressActivity.Type.ERROR);
            sendBroadcast(mUpdateIntent);
        }

        private void showSuccessNotification() {
            // Open file on click
            Intent intent = new Intent();
            intent.setAction(android.content.Intent.ACTION_VIEW);
            intent.setDataAndType(Uri.fromFile(mOutput), Utils.VIDEO_MIME);
            PendingIntent pendingIntent = PendingIntent.getActivity(getApplicationContext(), 0, intent,
                    PendingIntent.FLAG_UPDATE_CURRENT);

            // Show notification and a toast only if the progress for the current file isn't shown
            File progressingFile = ProgressActivity.getProgressingFile();
            if (progressingFile == null || progressingFile.compareTo(mOutput) != 0) {
                String str = String.format("android.resource://%s/%s", getPackageName(), R.raw.ok);
                mBuilder = new NotificationCompat.Builder(getApplicationContext());
                mBuilder.setContentTitle(Utils.getString(R.string.vc_finished))
                        .setTicker(Utils.getString(R.string.vc_finished))
                        .setContentText(Utils.getString(R.string.format_click_to_open_video, mOutput.getName()))
                        .setSmallIcon(R.drawable.ic_action_merge).setContentIntent(pendingIntent)
                        .setAutoCancel(true).setSound(Uri.parse(str));

                // Show the final notification
                NOTIFICATION_MANAGER.notify(sTaskCount, mBuilder.build());

                // Show a toast
                Toast.makeText(getApplicationContext(), R.string.clipped_see_notification, Toast.LENGTH_LONG)
                        .show();
            }

            // Send a broadcast message about the values update
            mUpdateIntent.putExtra(ProgressActivity.ARG_TYPE, ProgressActivity.Type.FINISHED);
            sendBroadcast(mUpdateIntent);
        }

        /**
         * Converts a time string to seconds.
         * @param outTime    string in format of hh:mm:ss:millis
         * @return -1 on error
         */
        private int timeToSecs(final String outTime) {
            // Remove microseconds
            int dotIndex = outTime.indexOf(".");
            if (dotIndex == -1)
                return -1;

            String[] time = outTime.substring(0, dotIndex).split(":");
            if (time.length != 3)
                return -1;

            try {
                int secs = 0;
                secs += Integer.parseInt(time[0]) * 60 * 60;
                secs += Integer.parseInt(time[1]) * 60;
                secs += Integer.parseInt(time[2]);
                return secs;
            } catch (NumberFormatException e) {
                return -1;
            }
        }

    }

}