style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

JavaFX: Creando un front end para ffmpeg | Haciendo ScreenCast.

Hola a todos, hoy les comparto mi pequeño y simple proyecto hecho en JavaFX, quise utilizar el SceneBuilder y los XML de FX, mas los CSS (de FX también).

Básicamente, la GUI solo actúa como un control para iniciar una grabación de audio y vídeo de nuestro entorno de escritorio. El verdadero poder lo tenemos en el software ffmpeg, el cual lanzamos como un proceso pero le hacemos algunas modificaciones mientras estamos en java.

El proyecto se compone de la siguiente manera:

Bien, cuando usamos la creacion de la GUI con FXML debemos tener tres cosas: La clase de control, el archivo fxml y nuestra clase principal.

Veamos pues;

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package controller;

import java.io.File;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.fxml.FXML;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.AnchorPane;
import javafx.stage.DirectoryChooser;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
import utils.ScreenRecordUtils;

/**
 *
 * @author kalt
 */

public class Controller {

    @FXML
    private Button bRecord, bSaveTo;
    @FXML
    private AnchorPane anchorPane;
    private static String path = null;
    private Stage s;
   
    public Controller() {
        ScreenRecordUtils.sizeWindow();
    }

    @FXML
    protected void clicButtonRecord(MouseEvent e) {
        s = (Stage) anchorPane.getScene().getWindow();
        if (bRecord.getText().equals("Record")) {
            s.setIconified(true);
            bRecord.setText("Stop");
            bSaveTo.setDisable(true);
           
            Task t = new Task() {
                @Override
                protected Void call() {
                    ScreenRecordUtils.runCommand();
                    return null;
                }
            };
            new Thread(t).start();
           
            Platform.setImplicitExit(false);
            s.setOnCloseRequest((WindowEvent event) -> {
               
               if(s.getTitle().equals("Recording...")) {
                   event.consume();
                   Alert alert = new Alert(Alert.AlertType.WARNING);
                   alert.setTitle("Operacion no permitida: Gabracion en curso.");
                   alert.setHeaderText("La grabacion sigue en curso.");
                   alert.setContentText("Debe parar la grabacion en el boton de Stop.");
                   alert.showAndWait();
               } else {
                   Platform.exit();
               }
            });
            s.setTitle("Recording...");
        } else if (bRecord.getText().equals("Stop")) {
            ScreenRecordUtils.commandExit();
            bRecord.setText("Record");
            bRecord.setDisable(true);
            bSaveTo.setDisable(false);
            s.setTitle("My ScreenCast");
        }
    }

    @FXML
    protected void clicButtonSave(MouseEvent event) {
        DirectoryChooser dc = new DirectoryChooser();
        dc.setTitle("Guardar en...");
        File selectDirectory = dc.showDialog(s);
        if (selectDirectory != null && selectDirectory.isDirectory()) {
            File[] listFiles = selectDirectory.listFiles();
            path = selectDirectory.getAbsolutePath();
            for (File f : listFiles) {
                if (f.toString().equals(path + "/out.mkv")) {
                    f.delete();
                }
            }
            ScreenRecordUtils.modifyCommand(path);
            bRecord.setDisable(false);
        }
    }
}

Nuestro archivo FXML

    <?xml version="1.0" encoding="UTF-8"?>

    <?import java.lang.*?>
    <?import java.util.*?>
    <?import javafx.scene.control.*?>
    <?import javafx.scene.layout.*?>
    <?import javafx.scene.paint.*?>

    <AnchorPane fx:id="anchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="310.0" prefWidth="339.0" xmlns:fx="http://javafx.com/fxml/1" xmlns="http://javafx.com/javafx/2.2" fx:controller="controller.Controller">
      <children>
        <Button fx:id="bSaveTo" layoutX="241.0" layoutY="268.0" mnemonicParsing="false" onMouseClicked="#clicButtonSave" text="Save to..." textAlignment="LEFT" textOverrun="CLIP" wrapText="false" />
        <Button fx:id="bRecord" disable="true" layoutX="111.0" layoutY="118.0" mnemonicParsing="false" onMouseClicked="#clicButtonRecord" text="Record" />
      </children>
    </AnchorPane>

Y nuestra clase principal:

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package main;

import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

/**
 *
 * @author kalt
 */

public class MainView extends Application {

    @Override
    public void start(Stage primaryStage) {
        configureUI(primaryStage);
    }

    private void configureUI(Stage s) {
        try {
            Parent root = FXMLLoader.load(getClass().getResource("/fxml/UI.fxml"));
            Scene scene = new Scene(root);
            scene.getStylesheets().add("/css/Style.css");
            s.setScene(scene);
            s.setResizable(false);

            s.centerOnScreen();
            s.setTitle("My ScreenCast");
            s.show();
        } catch (IOException ex) {
            Logger.getLogger(MainView.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Y unas cosillas extras, nuestro archivo css y una clase de utilidades:

/*
To change this license header, choose License Headers in Project Properties.
To change this template file, choose Tools | Templates
and open the template in the editor.
*/

/*
    Created on : 5/07/2016, 12:57:04 AM
    Author     : kalt
*/

.root {
    -fx-background-color:white;    
}

.button {
    -fx-padding: 10 10 15 15;
    -fx-background-insets: 0,0 0 5 0, 0 0 6 0, 0 0 7 0;
    -fx-background-radius: 8;
    -fx-background-color:  #00b300;        
    -fx-font-weight: bold;
    -fx-font-size: 2em;
    -fx-text-fill: white;
    -fx-text-alignment: center;
}
.button:hover {
    -fx-background-color: #cc0000;  
}
.button:pressed {
    -fx-padding: 10 15 13 15;
    -fx-background-insets: 2 0 0 0,2 0 3 0, 2 0 4 0, 2 0 5 0;
}

#bSaveTo {
    -fx-background-insets: 0,0 0 5 0, 0 0 6 0, 0 0 7 0;
    -fx-background-color:  #6666ff;  
    -fx-background-radius: 6;
    -fx-font-weight: bold;
    -fx-font-size: 0.85em;
    -fx-text-fill: white;
    -fx-text-alignment: center;
}

#bSaveTo:hover {
    -fx-background-color: #000099;
}
#bSaveTo:pressed {
    -fx-padding: 10 15 13 15;
    -fx-background-insets: 2 0 0 0,2 0 3 0, 2 0 4 0, 2 0 5 0;
 
}

/*
 * To change this license header, choose License Headers in Project Properties.
 * To change this template file, choose Tools | Templates
 * and open the template in the editor.
 */

package utils;

import java.awt.Dimension;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
import main.MainView;

/**
 *
 * @author kalt
 */

public class ScreenRecordUtils {

    private static String SIZE_WINDOW;
    private static String command = "ffmpeg -f alsa -ac 1 -i pulse -f x11grab -r 30 -s  -i :0.0 -acodec pcm_s16le -vcodec libx264 -preset ultrafast -crf 0 -threads 0 output";

    public static void sizeWindow() {
        Dimension screenSize = java.awt.Toolkit.getDefaultToolkit().getScreenSize();
        SIZE_WINDOW = screenSize.width + "x" + screenSize.height;
    }

    public static void modifyCommand(String path) {
        command = command.replace("-s", "-s " + SIZE_WINDOW);
        command = command.replace("output", path + "/out.mkv");
    }

    public static void runCommand() {
        try {
            commandExit();
            Process p = Runtime.getRuntime().exec(command);
            p.waitFor();
        } catch (IOException | InterruptedException ex) {
            Logger.getLogger(MainView.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public static void commandExit() {
        try {
            Process p = Runtime.getRuntime().exec("pkill ffmpeg");
            p.waitFor();
        } catch (IOException | InterruptedException ex) {
            Logger.getLogger(MainView.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}

Para muestra un video:

My ScreenCast demonstration from Kalt Wulx on Vimeo.

Recuerden que pueden descargar o clonar el proyecto desde git: les comparto el enlace y la pagina web.

Obviamente para mejores resultados es conveniente revisar la documentacion oficial de ffmpeg.
Me gustaria leer sus criticas y comentarios, me gustaria ademas me dieran algunos consejos para mejorar mi codigo.

AdjuntoTamaño
Screenshot_20160711_133540.png8.39 KB

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.

Github

No seria mejor que pusieras tu proyecto en Github ? , esta bueno :B

Imagen de Jose Manuel

Re:Github

Ya se encuentra en GitHub, en el post esta el enlace al repositorio.
Saludos.

Imagen de gabo

Sólo en *nix?

Hola José

Una duda ffmpeg sólo funciona en plataformas Linux? Si es así en mi caso prefiero usar comandos desde un archivo bash o shell script

Imagen de Jose Manuel

Re: Sólo en *nix

Nop, FFmpeg tiene soporte en Windows y en Mac también. Se que en Windows la instrucción no cambiaría mucho pero, no se que tanto en Mac.

Claro, el propósito es que yo diera inicio con algo, aunque fuera muy simple, de aquí tengo la esperanza de seguir añadiendo cosas interesantes (básicamente jugar con características de audio y vídeo), ademas me interesa mucho la cámara web y el soporte en Windows. En Mac no, porque, no tengo una.

Pero claro, si para ti basta un script pues sip. Eso seria lo mas simple de utilizar.
Saludos

Imagen de gabo

Gracias por el aporte

Gracias por la retro, buen ejercicio.

Imagen de gabo

Matar el proceso con SIGNAL 2 en Windows

Realice un port del proyecto hacia ambiente Windows (javax.Swing en lugar de JavaFX) y digo port ya que cuando ejecuto el programa FFMPEG (en un Thread aparte) note que cuando se mata el proceso se cierra el buffer pero de forma incorrecta y por ende el archivo de vídeo/audio queda corrupto y no se puede reproducir

Por ejemplo:

> tasklist /FI "ImageName eq ffmpeg.exe" /FI "STATUS eq running"
> taskkill /T /IM ffmpeg.exe /F

Al listar nuevamente los procesos activos se observa notablemente que el PID ya no existe (ha muerto) causando el inconveniente anteriormente expuesto, sin embargo cuando al proceso activo se le envia un SIGNAL 2 (Ctrl + C) se cierra de forma correcta y el archivo no queda corrupto. Encontré que hay un tema al respecto e incluso software de terceros (sendSignal)

No he realizado la prueba en ambiente *nix supongo que un kill -9 es suficiente.

Imagen de Jose Manuel

Tienes razon, con un pkill

Tienes razon, con un pkill -SIGTERM ffmpeg funciona, ya que la señal 15 (SIGTERM) es un cierre correcto.
Saludos.

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">