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
<?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.
Adjunto | Tamaño |
---|---|
Screenshot_20160711_133540.png | 8.39 KB |
- Jose Manuel's blog
- Inicie sesión o regístrese para enviar comentarios
Comentarios
Github
No seria mejor que pusieras tu proyecto en Github ? , esta bueno :B
Re:Github
Ya se encuentra en GitHub, en el post esta el enlace al repositorio.
Saludos.
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
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
Gracias por el aporte
Gracias por la retro, buen ejercicio.
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:
> 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.
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.