Menu
Last update: July 1, 2016 11:22
src
main
com
raspoid
AnalogComponent.java
Component.java
Config.java
GPIOComponent.java
GPIOPin.java
I2CComponent.java
I2CPin.java
PWMComponent.java
PWMPin.java
Pin.java
RaspberryPiModel.java
Tools.java
UARTPin.java
additionalcomponents
behavioral
brickpi
examples
exceptions
network
test

/*******************************************************************************
 * Copyright (c) 2016 Julien Louette & Gaƫl Wittorski
 * 
 * This file is part of Raspoid.
 * 
 * Raspoid is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Raspoid 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Raspoid.  If not, see <http://www.gnu.org/licenses/>.
 ******************************************************************************/
package com.raspoid.additionalcomponents.camera;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;

import com.raspoid.Tools;
import com.raspoid.examples.additionalcomponents.camera.CameraPiExample;
import com.raspoid.exceptions.RaspoidException;

/**
 * <b>This class is a wrapper for raspistill and raspivid, the command line tools
 * for capturing still photographs and videos with the camera module from Raspberry Pi.</b>
 * 
 * <p>Through this wrapper, you can easily
 *  <ul>
 *      <li>launch a preview window on your HDMI or PCB display</li>
 *      <li>take pictures</li>
 *      <li>take videos</li>
 *  </ul>
 * ... all of this with a panel of usefull parameters.</p>
 * 
 * <p>We provide you with a set of predefined constructors to easily use all the available options.</p>
 * 
 * <h3>Setting up camera module</h3>
 * <p>Before using this, you need to enable camera support on your Raspberry Pi.
 * This can be done by executing the following instructions on the command line.</p>
 * <p>First, download and install the latest kernel, GPU firmware and applications.
 * You will need an internet connection for this to work correctly.</p>
 *      <pre>sudo apt-get update sudo apt-get upgrade</pre>
 * <p>Now you need to enable camera support using the raspi-config.<p>
 *      <pre>sudo raspi-config</pre>
 * <p>Use the cursor keys to move to the camera option and select "enable".</p>
 * <p>On exiting raspi-config it will ask to reboot. The enable option will ensure that 
 * on reboot the correct GPU firmware will be running (with the camera driver and tuning),
 * and the GPU memory split is sufficient to allow the camera to acquire enough memory
 * to run correctly.</p>
 * 
 * <h3>Notes</h3>
 * <ul>
 *  <li>The camera adds about 200-250mA to the power requirements of your Raspberry Pi.
 * </uL>
 * 
 * <h3>Infos</h3>
 * <ul>
 *  <li>https://www.raspberrypi.org/documentation/raspbian/applications/camera.md</li>
 *  <li>https://github.com/raspberrypi/userland/tree/master/host_applications/linux/apps/raspicam</li>
 * </ul>
 * 
 * <p>Example of use: {@link CameraPiExample}</p>
 * 
 * @author Julien Louette &amp; Ga&euml;l Wittorski
 * @version 1.0
 */
public class CameraPi {
    
    /* ----------------------------------------------------------
     *                          UTILS
     * ----------------------------------------------------------*/
    
    /**
     * Application ran successfully.
     */
    private static final int EX_OK = 0;
    
    /**
     * Bad command line parameter.
     */
    private static final int EX_USAGE = 64;
    
    /**
     * Software or camera error.
     */
    private static final int EX_SOFTWARE = 70;
    
    /**
     * Application terminated by ctrl-C.
     */
    private static final int APPLICATION_TERMINATED_BY_CTRL_C = 130;
    
    /**
     * Private constructor to hide the implicit public one.
     */
    private CameraPi() {
    }
    
    /**
     * Executes the specified string command in a separate process.
     * @param command the string command to execute.
     * @return an exit value. EX_OK if command ran successfully,
     * EX_USAGE if bad command line parameter,
     * EX_SOFTWARE if software or camera error,
     * APPLICATION_TERMINATED_BY_CTRL_C if application terminated by ctrl-C
     * or other for other type of error.
     */
    public static int executeCommand(String command) {
        try {
            Tools.debug("[CameraPi] executed command: " + command, Tools.Color.ANSI_BLUE);
            Process process = Runtime.getRuntime().exec(command);
            process.waitFor();
            int exitValue = process.exitValue();
            switch(exitValue) {
            case EX_OK:
                Tools.debug("[CameraPi] Command ran successfully", Tools.Color.ANSI_BLUE);
                break;
            case EX_USAGE:
                Tools.debug("[CameraPi] Bad command line parameter", Tools.Color.ANSI_BLUE);
                break;
            case EX_SOFTWARE:
                Tools.debug("[CameraPi] Software or camera error", Tools.Color.ANSI_BLUE);
                break;
            case APPLICATION_TERMINATED_BY_CTRL_C:
                Tools.debug("[CameraPi] Application terminated by ctrl-C", Tools.Color.ANSI_BLUE);
                break;
            default:
                Tools.debug("[CameraPi] Error when executing command. Exit value=" + exitValue, Tools.Color.ANSI_BLUE);
                break;
            }
            return exitValue;
        } catch (IOException | InterruptedException e) {
            throw new RaspoidException("Error when executing command: " + command, e);
        }
    }
    
    /* ----------------------------------------------------------
     *                      PREVIEW WINDOW
     * ----------------------------------------------------------*/
    
    /**
     * Enable a fullscreen preview for a duration of millis milliseconds.
     * <p>The preview can only be done with an HDMI display (directly connected to the Pi),
     * or a display connected through the PCB display port (we couldn't test this).
     * A vnc connection is not sufficient.</p>
     * @param millis the duration of the preview, in milliseconds.
     */
    public static void preview(int millis) {
        PreviewConfig previewConfig = new PreviewConfig();
        previewConfig.enablePreviewFullscreen();
        preview(previewConfig, millis);
    }
    
    /**
     * Enable a preview with specific config, for a default duration of 5000 milliseconds.
     * @param previewConfig the preview configuration to apply on the image.
     */
    public static void preview(PreviewConfig previewConfig) {
        preview(previewConfig, PictureConfig.DEFAULT_TIMEOUT);
    }
    
    /**
     * Enable a preview with specific preview configuration,
     * and for a duration of millis milliseconds.
     * @param previewConfig the preview configuration to apply on the image.
     * @param millis the duration of the preview, in milliseconds.
     */
    public static void preview(PreviewConfig previewConfig, int millis) {
        if(millis < PictureConfig.MIN_TIMEOUT)
            millis = PictureConfig.MIN_TIMEOUT;
        String command = PictureConfig.BASE_COMMAND;
        command += " -t " + millis;
        command += previewConfig.getPreviewSettings();
        executeCommand(command);
    }
    
    /* ----------------------------------------------------------
     *                          PICTURES
     * ----------------------------------------------------------*/
    
    /**
     * Takes a picture as soon as possible, with default parameter values.
     * <p>No preview.</p>
     * @return a new Picture object, corresponding to the newly taken picture.
     */
    public static Picture takePicture() {
        return takePicture(new PictureConfig());
    }
    
    /**
     * Takes a new picture with specified width, height and quality.
     * Other parameters are defaults.
     * <p>No preview.</p>
     * @param width the width of the new picture, in the [{@link PictureConfig#MIN_WIDTH}, {@link PictureConfig#MAX_WIDTH}] range.
     * @param height the height of the new picture, in the [{@link PictureConfig#MIN_HEIGHT}, {@link PictureConfig#MAX_HEIGHT}] range.
     * @param quality the quality of the new picture, in the [{@link PictureConfig#MIN_QUALITY}, {@link PictureConfig#MAX_QUALITY}] range.
     * @return a new Picture object, corresponding to the newly taken picture.
     */
    public static Picture takePicture(int width, int height, int quality) {
        return takePicture(new PictureConfig(width, height, quality));
    }
    
    /**
     * Takes a new picture with a custom PictureConfig.
     * <p>No preview.</p>
     * @param config the PictureConfig to apply to take the picture.
     * @return a new Picture object, corresponding to the newly taken picture.
     */
    public static Picture takePicture(PictureConfig config) {
        PreviewConfig previewConfig = new PreviewConfig();
        previewConfig.disablePreview();
        return takePicture(config, previewConfig);
    }
    
    /**
     * Takes a new picture with a custom PictureConfig,
     * and a PreviewConfig to enable a preview in the same time.
     * @param pictureConfig the PictureConfig to apply to take the picture.
     * @param previewConfig the PreviewConfig to apply to take the picture.
     * @return a new Picture object, corresponding to the newly taken picture.
     */
    public static Picture takePicture(PictureConfig pictureConfig, PreviewConfig previewConfig) {
        String command = pictureConfig.getCommand();
        command += previewConfig.getPreviewSettings();
        executeCommand(command);
        String filePath = System.getProperty("user.dir") + "/" + pictureConfig.getOutputFilenameWithExtension();
        return new Picture(pictureConfig, filePath);
    }
    
    /* ----------------------------------------------------------
     *                          VIDEOS
     * ----------------------------------------------------------*/
    
    /**
     * Takes a new video of millis milliseconds.
     * <p>No preview.</p>
     * @param millis the duration of the video, in milliseconds.
     * @return a new Video object, corresponding to the newly taken video.
     */
    public static Video takeVideo(int millis) {
        return takeVideo(new VideoConfig(millis));
    }
    
    /**
     * Takes a new video for duration of millis milliseconds, with specified width and height.
     * Other parameters are defaults.
     * <p>No preview.</p>
     * @param millis the duration of the video, in milliseconds.
     * @param width the width of the new video.
     * @param height rge height of the new video.
     * @return a new Video object, corresponding to the newly taken video.
     */
    public static Video takeVideo(int millis, int width, int height) {
        return takeVideo(new VideoConfig(millis, width, height));
    }
    
    /**
     * Takes a new video with the specified VideoConfig.
     * <p>No preview.</p>
     * @param config the VideoConfig to apply to take the video.
     * @return a new Video object, corresponding to the newly taken video.
     */
    public static Video takeVideo(VideoConfig config) {
        PreviewConfig previewConfig = new PreviewConfig();
        previewConfig.disablePreview();
        return takeVideo(config, previewConfig);
    }
    
    /**
     * Takes a new video with the specific VideoConfig and a PreviewConfig to enable a preview in the same time.
     * @param videoConfig the VideoConfig to apply to take the video.
     * @param previewConfig the PreviewConfig to apply to take the video.
     * @return a new Video object, corresponding to the newly taken video.
     */
    public static Video takeVideo(VideoConfig videoConfig, PreviewConfig previewConfig) {
        String command = videoConfig.getCommand();
        command += previewConfig.getPreviewSettings();
        executeCommand(command);
        String filePath = System.getProperty("user.dir") + "/" + videoConfig.getOutputFilenameWithExtension();
        return new Video(videoConfig, filePath);
    }
    
    /* ----------------------------------------------------------
     *                        STREAMING
     * ----------------------------------------------------------*/
    
    /**
     * Creates a new video stream from the Camera Pi, on a specific IP address:port,
     * using the GStreamer tool (v0.10).
     * 
     * <p>There is no need to stop this GStreamer server. It will shut down when the java process will end.</p>
     * 
     * @param ipAddress the IP address linked to the video stream.
     * @param port the port linked to the video stream.
     * @param width the width of the video stream. Higher is better, but requires larger network load.
     * @param height the height of the video stream. Higher is better, but requires larger network load.
     * @param vflip true to flip the image vertically. False otherwise.
     * @param hflip true to flip the image horizontally. False otherwise.
     * @param bitrate the bitrate used for the video stream.
     * @param preview true to enable a fullscreen preview.
     * @param verbose true to print gstreamer through Tools.log system.
     */
    public static void startGStreamerServer(String ipAddress, int port, int width, int height, 
            boolean vflip, boolean hflip, int bitrate, boolean preview, boolean verbose) {
        VideoConfig videoConfig = new VideoConfig(0, width, height);
        videoConfig.setOutputFilename("-");
        videoConfig.setFramerate(25);
        videoConfig.setVerticalFlip(vflip);
        videoConfig.setHorizontalFlip(hflip);
        videoConfig.setBitrate(bitrate);
        String command = videoConfig.getCommand();
        if(preview)
            command += " -f";
        
        command += " | gst-launch -v fdsrc ! h264parse ! rtph264pay config-interval=1 pt=96 ! gdppay ! tcpserversink host=" + ipAddress + " port=" + port;
        
        // execute command. Because of piped commands, we need to write a script
        // and execute the script instead of separate commands.
        // Pipe is part of the shell, so we can use the following to do the trick.
        try {
            String[] cmd = {"/bin/sh", "-c", command};
            Process process = Runtime.getRuntime().exec(cmd);
            Tools.log("GStreamer server launching.", Tools.Color.ANSI_GREEN);
            
            if(verbose) {
                new Thread(()->readInputStreamLines(process)).start();
            }
            
            Tools.sleepMilliseconds(2000);
            Tools.log("GStreamer server launched. You can read it on client side with the following:"
                    + "\n\tgst-launch-1.0 -v tcpclientsrc host=" + ipAddress + " port=" + port + 
                    "  ! gdpdepay ! rtph264depay ! avdec_h264 ! videoconvert ! autovideosink sync=false", Tools.Color.ANSI_GREEN);

            process.waitFor();
            int exitValue = process.exitValue();
            Tools.debug("GStreamer server stopped. (Exit value=" + exitValue + ")");
        } catch(IOException | InterruptedException e) {
            throw new RaspoidException("Error when executing command: " + command, e);
        }
    }
    
    /**
     * Method created to respect Java metrics (Sonar lint issue asking to create a new method for this).
     * @param process the process used to read input stream from.
     */
    private static void readInputStreamLines(Process process) {
        try (BufferedReader input = new BufferedReader(new InputStreamReader(process.getInputStream(), StandardCharsets.UTF_8))) {
            String line; 
            while ((line = input.readLine()) != null)
               Tools.log(line);
        } catch (IOException e) {
            Tools.log("Error when printing verbose lines from gstreamer. (Exception=" + e + ")");
        }
    }
}