/*
* ShootOFF - Software for Laser Dry Fire Training
* Copyright (C) 2016 phrack
*
* 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
* (at your option) 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.shootoff.gui.controller;
import java.awt.image.BufferedImage;
import java.io.File;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.TimeUnit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.shootoff.gui.PlaybackListener;
import com.xuggle.mediatool.IMediaReader;
import com.xuggle.mediatool.MediaListenerAdapter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.mediatool.event.IVideoPictureEvent;
import com.xuggle.xuggler.IContainer;
import com.xuggle.xuggler.IError;
import javafx.application.Platform;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import com.shootoff.util.SwingFXUtils;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.Slider;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.stage.Stage;
public class VideoPlayerController implements PlaybackListener {
@FXML private TabPane videoTabPane;
@FXML private Slider timeSlider;
@FXML private Label timeLabel;
@FXML private Button togglePlaybackButton;
private static final Logger logger = LoggerFactory.getLogger(VideoPlayerController.class);
private final Map<String, PlaybackContext> contexts = new HashMap<>();
private PlaybackContext currentContext;
public void init(Map<String, File> videos) {
togglePlaybackButton.setGraphic(new ImageView(
new Image(VideoPlayerController.class.getResourceAsStream("/images/gnome_media_playback_start.png"))));
createTabs(videos);
currentContext = contexts.get(videoTabPane.getSelectionModel().getSelectedItem().getText());
timeSlider.setMax(currentContext.getDuration());
timeSlider.valueProperty().addListener(new ChangeListener<Number>() {
@Override
public void changed(ObservableValue<? extends Number> observableValue, Number oldValue, Number newValue) {
if (newValue == null) {
timeLabel.setText("");
return;
}
setTime(newValue.longValue());
}
});
videoTabPane.getSelectionModel().selectedItemProperty().addListener(new ChangeListener<Tab>() {
@Override
public void changed(ObservableValue<? extends Tab> observable, Tab oldValue, Tab newValue) {
togglePlaybackButton.setGraphic(new ImageView(new Image(
VideoPlayerController.class.getResourceAsStream("/images/gnome_media_playback_start.png"))));
currentContext.pausePlayback();
currentContext = contexts.get(newValue.getText());
timeSlider.setMax(currentContext.getDuration());
timeSlider.setValue(currentContext.getTimestamp());
}
});
}
private void setTime(long timestamp /* ms */) {
final Date date = new Date(timestamp);
final DateFormat formatter = new SimpleDateFormat("mm:ss:SSS");
timeLabel.setText(formatter.format(date));
}
@Override
public void frameUpdated(long timestamp) {
timeSlider.setValue(timestamp);
if (timestamp == currentContext.getDuration()) {
togglePlaybackButton.setGraphic(new ImageView(new Image(
VideoPlayerController.class.getResourceAsStream("/images/gnome_media_playback_start.png"))));
}
}
private static class PlaybackContext extends MediaListenerAdapter {
private final IMediaReader mediaReader;
private final PlaybackListener listener;
private final long duration;
private boolean isPlaying = false;
private final ImageView imageView = new ImageView();
private boolean doDelay = true;
private long lastTimestamp = 0;
public PlaybackContext(File videoFile, PlaybackListener listener) {
this.listener = listener;
mediaReader = ToolFactory.makeReader(videoFile.getPath());
mediaReader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);
mediaReader.open();
duration = mediaReader.getContainer().getDuration() / 1000; // microseconds
// to
// milliseconds
mediaReader.addListener(this);
}
public long getDuration() {
return duration;
}
public long getTimestamp() {
return lastTimestamp;
}
@Override
public void onVideoPicture(IVideoPictureEvent event) {
final long currentTimestamp = event.getTimeStamp(TimeUnit.MILLISECONDS);
if (doDelay) {
try {
final long delay = currentTimestamp - lastTimestamp;
Thread.sleep(delay);
} catch (final InterruptedException e) {
logger.error("Error while reading video frames", e);
}
}
lastTimestamp = currentTimestamp;
imageView.setImage(SwingFXUtils.toFXImage(event.getImage(), null));
if (isPlaying || !doDelay) Platform.runLater(() -> listener.frameUpdated(currentTimestamp));
}
private void playVideo() {
new Thread(() -> {
IError ret = mediaReader.readPacket();
while (isPlaying && ret == null) {
ret = mediaReader.readPacket();
}
// ret is null if movie was paused
if (ret != null && ret.getType() == IError.Type.ERROR_EOF) {
isPlaying = false;
lastTimestamp = getDuration();
Platform.runLater(() -> listener.frameUpdated(getDuration()));
}
}, "PlayVideo").start();
}
private void playFromBeginning() {
lastTimestamp = 0;
mediaReader.open();
mediaReader.getContainer().seekKeyFrame(0, 0, 0, 0, IContainer.SEEK_FLAG_ANY);
playVideo();
}
public void nextFrame() {
doDelay = false;
mediaReader.readPacket();
doDelay = true;
}
public void pausePlayback() {
isPlaying = false;
}
public void togglePlayback() {
isPlaying = !isPlaying;
if (isPlaying) {
if (lastTimestamp != getDuration()) {
playVideo();
} else {
playFromBeginning();
}
}
}
public boolean isPlaying() {
return isPlaying;
}
public ImageView getImageView() {
return imageView;
}
}
private void createTabs(Map<String, File> videos) {
for (final Entry<String, File> video : videos.entrySet()) {
final Tab videoTab = new Tab(video.getKey());
videoTabPane.getTabs().add(videoTab);
final PlaybackContext context = new PlaybackContext(video.getValue(), this);
videoTab.setContent(context.getImageView());
contexts.put(video.getKey(), context);
}
}
@FXML
public void nextButtonClicked(ActionEvent event) {
currentContext.nextFrame();
}
@FXML
public void togglePlaybackButtonClicked(ActionEvent event) {
currentContext.togglePlayback();
if (currentContext.isPlaying()) {
togglePlaybackButton.setGraphic(new ImageView(new Image(
VideoPlayerController.class.getResourceAsStream("/images/gnome_media_playback_pause.png"))));
} else {
togglePlaybackButton.setGraphic(new ImageView(new Image(
VideoPlayerController.class.getResourceAsStream("/images/gnome_media_playback_start.png"))));
}
}
public Stage getStage() {
return (Stage) togglePlaybackButton.getScene().getWindow();
}
}