com.github.thebigs.foscam.recorder.Recorder.java Source code

Java tutorial

Introduction

Here is the source code for com.github.thebigs.foscam.recorder.Recorder.java

Source

/**
 *  foscamRecorder - http://github.com/TheBigS
 *  Copyright (C) 2012
 *
 *  This program 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
 *   any later version.
 *
 *  This program 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 this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.github.thebigs.foscam.recorder;

import java.awt.Image;
import java.awt.Toolkit;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.GnuParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.ParseException;
import org.apache.commons.lang.StringUtils;
import org.apache.tools.ant.DirectoryScanner;

public class Recorder implements Runnable {
    public static final String DEFAULT_CAM_NAME = "webcam";
    public static final String DEFAULT_OUTPUT_DIR = "./";
    public static final Long DEFAULT_MAX_DISK_SPACE_MB = -1L;
    public static final Long DEFAULT_CYCLE_DURATION_MINS = 60L;

    public void setDefaults() {
        this.camName = DEFAULT_CAM_NAME;
        this.outputDir = DEFAULT_OUTPUT_DIR;
        this.cycleDurationMins = DEFAULT_CYCLE_DURATION_MINS;
        this.maxDiskSpaceMb = DEFAULT_MAX_DISK_SPACE_MB;
    }

    private String camUrl;
    private String camName;
    private String outputDir;
    private long cycleDurationMins;
    private long maxDiskSpaceMb;

    private List<WebCamImageListener> imageListeners = new ArrayList<WebCamImageListener>();
    private volatile boolean shutdown = false;
    private Recording currentRecording = null;
    private long totalBytesSaved = 0L;

    public Recorder(String camUrl) {
        super();
        setDefaults();

        if (!StringUtils.isBlank(camUrl)) {
            this.camUrl = camUrl;
        } else {
            throw new IllegalArgumentException("Cam URL cannot be blank.");
        }
    }

    public Recorder(String camUrl, String camName, String outputDir, Long cycleDurationMins, Long maxDiskSpaceMb) {
        super();
        setDefaults();

        if (!StringUtils.isBlank(camUrl)) {
            this.camUrl = camUrl;
        } else {
            throw new IllegalArgumentException("Cam URL cannot be blank.");
        }

        if (!StringUtils.isBlank(camName)) {
            this.camName = camName;
        }
        if (!StringUtils.isBlank(outputDir)) {
            this.outputDir = outputDir;
        }
        if (cycleDurationMins != null) {
            this.cycleDurationMins = cycleDurationMins;
        }
        if (maxDiskSpaceMb != null) {
            this.maxDiskSpaceMb = maxDiskSpaceMb;
        }
    }

    private void initTotalBytesSaved() {
        // initialize the totalBytesSaved
        String[] files = getRecordedVideoFiles();
        for (String file : files) {
            totalBytesSaved += new File(outputDir + "/" + file).length();
        }
    }

    public void addWebCamImageListener(WebCamImageListener l) {
        synchronized (imageListeners) {
            if (!imageListeners.contains(l)) {
                imageListeners.add(l);
            }
        }
    }

    public boolean removeWebCamImageListener(WebCamImageListener l) {
        synchronized (imageListeners) {
            return imageListeners.remove(l);
        }
    }

    public void clearWebCamImageListeners() {
        synchronized (imageListeners) {
            imageListeners.clear();
        }
    }

    public void shutdown() {
        shutdown = true;
        if (currentRecording != null) {
            currentRecording.close();
        }
        clearWebCamImageListeners();
    }

    @Override
    public void run() {
        new File(outputDir).mkdirs();
        initTotalBytesSaved();

        while (!shutdown) {
            try {
                URL url = new URL(camUrl);

                // Open a Connection to the server
                URLConnection urlc = url.openConnection();
                if (urlc == null) {
                    throw new IOException("Unable to make a connection to the image source");
                }
                // Turn off caches to force fresh reload of the jpg
                urlc.setUseCaches(false);
                urlc.connect(); // ignored if already connected.
                InputStream stream = urlc.getInputStream();

                // Line 1: "--ipcamera"
                String delimiter = new String(readLine(stream));
                while (!shutdown && !imageListeners.isEmpty()) {
                    // Line 2: "Content-Type: image/jpeg"
                    String contentType = new String(readLine(stream)).split(":")[1].trim();
                    // Line 3: "Content-Length: 23304"
                    int contentLength = Integer.parseInt(new String(readLine(stream)).split(":")[1].trim());
                    // Line 4: <Blank Line>
                    readLine(stream);

                    // read image data
                    byte[] imageData = new byte[contentLength];
                    for (int i = 0; i < contentLength; i++) {
                        int readByte = stream.read();
                        imageData[i] = (byte) readByte;
                    }

                    Image image = Toolkit.getDefaultToolkit().createImage(imageData);
                    // save the image to the current recording
                    saveImage(image);

                    // notify listeners
                    synchronized (imageListeners) {
                        for (WebCamImageListener l : imageListeners) {
                            l.onImage(image);
                        }
                    }

                    // read stream till next delimiter
                    boolean delimiterFound = false;
                    ByteArrayOutputStream delimiterBuffer = new ByteArrayOutputStream();
                    while (!delimiterFound) {
                        int nextByte = stream.read();
                        if (nextByte == 0x2d) {
                            int followingByte = stream.read();
                            if (followingByte == 0x2d) {
                                // read 8 bytes into the delimiter buffer
                                for (int i = 0; i < 8; i++) {
                                    delimiterBuffer.write(stream.read());
                                }
                                if (new String(delimiterBuffer.toByteArray()).equals("ipcamera")) {
                                    delimiterFound = true;
                                    // skip CR/LF
                                    stream.skip(2);
                                }
                            }
                        }
                    }
                }
            } catch (MalformedURLException e) {
                System.err.println("Unable to parse URL: '" + camUrl + "'");
                System.exit(-1);
            } catch (IOException e) {
                System.err
                        .println("IO Exception: server not responding at : '" + camUrl + "'. retrying in 1 second");

                // sleep for 1 sec
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e1) {
                }
                continue;
            }

            if (imageListeners.isEmpty()) {
                // sleep for 5 secs
                try {
                    Thread.sleep(5000);
                } catch (InterruptedException e) {
                }
            }
        }
    }

    private void saveImage(Image image) {
        Date currTime = new Date();
        if (currentRecording == null) {
            currentRecording = new Recording(outputDir, camName, currTime);
            System.out.println("Recording file: opening " + currentRecording.getRecordingFileLocation());
        }
        // check if we need to cycle the recording file
        else if (currTime.getTime() - currentRecording.getStartTime() >= TimeUnit.MINUTES
                .toMillis(cycleDurationMins)) {
            System.out.println("Cycling Recording file: closing " + currentRecording.getRecordingFileLocation());
            currentRecording.close();
            totalBytesSaved += currentRecording.getRecordingFileSize();
            currentRecording = new Recording(outputDir, camName, currTime);
            System.out.println("Cycling Recording file: opening " + currentRecording.getRecordingFileLocation());
        }
        currentRecording.saveImage(image);

        // check for max disk space
        if (maxDiskSpaceMb >= 0) {
            double totalMbUsed = ((totalBytesSaved + currentRecording.getRecordingFileSize()) / 1024.0) / 1024.0;
            if (totalMbUsed >= maxDiskSpaceMb - 5.0) { //5mb ceiling so we don't go over the limit
                System.out.println("Deleting oldest Recording.. finding oldest file");
                deleteOldestRecording();
            }
        }
    }

    private void deleteOldestRecording() {
        String[] files = getRecordedVideoFiles();
        if (files.length > 0) {
            String recordingToDelete = outputDir + "/" + files[0];
            System.out.println("Deleting oldest Recording: " + recordingToDelete);
            File file = new File(recordingToDelete);
            totalBytesSaved -= file.length();
            file.delete();
        }
    }

    private String[] getRecordedVideoFiles() {
        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setIncludes(new String[] { "*.mp4" });
        scanner.setBasedir(outputDir);
        scanner.setCaseSensitive(false);
        scanner.scan();

        String[] mp4Files = scanner.getIncludedFiles();

        List<String> validRecordedVideoFiles = new ArrayList<String>();
        for (String mp4File : mp4Files) {
            try {
                Recording.FILE_DATE_FORMAT.parse(mp4File);
                if (mp4File.endsWith(camName + ".mp4")) {
                    validRecordedVideoFiles.add(mp4File);
                }
            } catch (java.text.ParseException e) {
                // didn't match
            }
        }

        Collections.sort(validRecordedVideoFiles, new Comparator<String>() {
            @Override
            public int compare(String arg0, String arg1) {
                try {
                    Date date0 = Recording.FILE_DATE_FORMAT.parse(arg0);
                    Date date1 = Recording.FILE_DATE_FORMAT.parse(arg1);

                    return date0.compareTo(date1);
                } catch (java.text.ParseException e) {
                    e.printStackTrace();
                }
                return 0;
            }
        });

        String[] recordedVideoFilesSorted = new String[validRecordedVideoFiles.size()];
        for (int i = 0; i < validRecordedVideoFiles.size(); i++) {
            recordedVideoFilesSorted[i] = validRecordedVideoFiles.get(i);
        }
        return recordedVideoFilesSorted;
    }

    private byte[] readLine(InputStream stream) throws IOException {
        ByteArrayOutputStream buffered = new ByteArrayOutputStream();

        while (true) {
            int nextByte = stream.read();

            // carrige return
            if (nextByte == 0x0d) {
                int lineFeed = stream.read();
                if (lineFeed != 0x0a) {
                    System.err.println("Parse error: found 0x0d not followed by 0x0a");
                } else {
                    break;
                }
            } else {
                buffered.write(nextByte);
            }
        }
        return buffered.toByteArray();
    }

    @Override
    public String toString() {
        return "Record [camUrl=" + camUrl + ", camName=" + camName + ", outputDir=" + outputDir
                + ", cycleDurationMins=" + cycleDurationMins + ", maxDiskSpace=" + maxDiskSpaceMb + "]";
    }

    // Command line app
    public static void main(String[] args) {
        Options options = new Options();

        options.addOption("c", true, "Cam url (eg. http://<ip>:<port>/<stream page>?<params>");
        options.addOption("o", true,
                "Output dir location (default: './'). File names will default to '<YYYY.MM.DD-mm-ss>-<webcam name>.mp4'.");
        options.addOption("n", true, "Webcam name. Defaults to 'webcam'.");
        options.addOption("d", true, "Duration (in mins) before cycling to new video file (Defaults to 60mins).");
        options.addOption("x", true,
                "Max disk space (in megabytes [1024kb]) to use before overwritting recordings. Oldest recordings will be overwritten first. Defaults to -1 (unlimited)");
        options.addOption("h", false, "Print this help message.");

        try {
            CommandLine cli = new GnuParser().parse(options, args);

            String camUrl = null;
            String camName = null;
            String outputDir = null;
            Long durationMins = null;
            Long maxDiskSpaceMb = null;

            if (cli.hasOption("h")) {
                HelpFormatter formatter = new HelpFormatter();
                formatter.printHelp("java -jar recorder.jar", options);
                System.exit(-1);
            }
            if (cli.hasOption("c")) {
                camUrl = cli.getOptionValue("c");
            } else {
                System.err.println("Cam Url (-c) is required. Use -h for more information.");
                System.exit(-1);
            }
            if (cli.hasOption("n")) {
                camName = cli.getOptionValue("n");
            }
            if (cli.hasOption("o")) {
                outputDir = cli.getOptionValue("o");
            }
            if (cli.hasOption("d")) {
                durationMins = Long.parseLong(cli.getOptionValue("d"));
            }
            if (cli.hasOption("x")) {
                maxDiskSpaceMb = Long.parseLong(cli.getOptionValue("x"));
            }

            Recorder recording = new Recorder(camUrl, camName, outputDir, durationMins, maxDiskSpaceMb);

            System.out.println("Starting recording...");
            recording.run(); // stay here until program is terminated
        } catch (ParseException e) {
            System.err.println("Unable to parse command line options: " + e.getMessage());
        } catch (Exception e) {
            System.err.println("Unable to parse command line options: " + e.getMessage());
        }
    }
}