/* This file is part of Wattzap Community Edition. * * Wattzap Community Edtion 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. * * Wattzap Community Edition 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 Wattzap. If not, see <http://www.gnu.org/licenses/>. */ package com.wattzap.view; import java.awt.Canvas; import java.awt.Color; import java.awt.Rectangle; import java.awt.Robot; import java.awt.image.BufferedImage; import java.io.File; import java.math.BigDecimal; import java.math.RoundingMode; import javax.imageio.ImageIO; import javax.swing.ImageIcon; import javax.swing.JFrame; import javax.swing.JPanel; import org.apache.log4j.LogManager; import org.apache.log4j.Logger; import uk.co.caprica.vlcj.player.MediaPlayerFactory; import uk.co.caprica.vlcj.player.embedded.DefaultFullScreenStrategy; import uk.co.caprica.vlcj.player.embedded.EmbeddedMediaPlayer; import uk.co.caprica.vlcj.player.embedded.FullScreenStrategy; import com.wattzap.controller.MessageBus; import com.wattzap.controller.MessageCallback; import com.wattzap.controller.Messages; import com.wattzap.model.RouteReader; import com.wattzap.model.UserPreferences; import com.wattzap.model.dto.Point; import com.wattzap.model.dto.Telemetry; import com.wattzap.utils.Rolling; /** * (c) 2013-2016 David George / www.WattzAp.com * * Video Player. * * Synchronizes video playback to road speed. This is done using an SDK to the * cross platform VLC player. We can't set the speed frame by frame because it * would be too expensive in terms of CPU cycles. What we do is compare the * speed from the rider with the speed the video was recorded at and set the * playback speed according to this ratio. For example is the rider is doing * 10kph and the video was recorded at 20kpg we set the playback speed to 50%. * * It sounds easy but both the rider speed and video record speed are constantly * varying so adjustments have to be made continuously. It is very easy to * overshoot hence the different offsets used. * * @author David George * @date 11 June 2013 */ public class VideoPlayer extends JFrame implements MessageCallback { private static final long serialVersionUID = 8813937587710441310L; private EmbeddedMediaPlayer mPlayer; private MediaPlayerFactory mediaPlayerFactory; private Canvas canvas; private ScreenGrabber grabber = null; int imageCount = 0; long startTime = 0; long mapStartTime; long lastMapTime; long lastVideoTime; RouteReader routeData; long len; float fps; JPanel odo; JFrame mainFrame; boolean videoLoaded; Rolling rSpeed; Rolling rRate; float diff; float lastRate; float lastCent; private static Logger logger = LogManager.getLogger("Video Player"); public VideoPlayer(JFrame main, JPanel odo) { super(); this.odo = odo; this.mainFrame = main; setTitle("Video - www.WattzAp.com"); // setUndecorated(true); ImageIcon img = new ImageIcon("icons/video.jpg"); setIconImage(img.getImage()); /* Messages we are interested in */ // MessageBus.INSTANCE.register(Messages.SPEED, this); MessageBus.INSTANCE.register(Messages.STARTPOS, this); MessageBus.INSTANCE.register(Messages.STOP, this); MessageBus.INSTANCE.register(Messages.CLOSE, this); MessageBus.INSTANCE.register(Messages.GPXLOAD, this); } public void init() { mediaPlayerFactory = new MediaPlayerFactory(); canvas = new java.awt.Canvas(); canvas.setBackground(Color.BLACK); this.add(canvas, java.awt.BorderLayout.CENTER); mediaPlayerFactory.newVideoSurface(canvas); FullScreenStrategy fullScreenStrategy = new DefaultFullScreenStrategy( this); mPlayer = mediaPlayerFactory.newEmbeddedMediaPlayer(fullScreenStrategy); mPlayer.setVideoSurface(mediaPlayerFactory.newVideoSurface(canvas)); setBounds(UserPreferences.INSTANCE.getVideoBounds()); setVisible(false); } private void setSpeed(Telemetry t) { Point p = routeData.getPoint(t.getDistanceKM()); if (startTime == 0) { // first time through, start video startTime = System.currentTimeMillis(); mPlayer.start(); mPlayer.enableOverlay(true); mPlayer.mute(); fps = mPlayer.getFps(); len = mPlayer.getLength(); diff = 1.0f; float time = len / fps; // s = d / t // we have the average speed of clip and our current speed, set // initial rate logger.debug("FPS " + fps + " length " + len + " milliSeconds time " + time); rSpeed.add(p.getSpeed()); double rate = t.getSpeedKMH() / p.getSpeed(); if (rate > 1.0) { rate = 1.0f; } // sets position to between 0 - 1 (end) if (mapStartTime > 0) { float pos = (float) mapStartTime / len; mPlayer.setPosition(pos); } mPlayer.setRate((float) rate); // initial rate // mapStartTime -= p.getTime(); // mapStartTime = 0; lastMapTime = 0 + mapStartTime; lastCent = 1.0f; return; } if (mPlayer.isPlaying() == false) { mPlayer.start(); } if (t.getSpeedKMH() == 0.0) { mPlayer.pause(); return; } // mPlayer.setMarqueeText("hello world"); // mPlayer.setMarqueeOpacity(127); // mPlayer.setMarqueeColour(Color.RED); // mPlayer.setMarqueePosition(libvlc_marquee_position_e.centre); // mPlayer.enableMarquee(true); long mapTime = p.getTime(); long videoTime = (int) (len * mPlayer.getPosition()); if (videoTime == 0) { return; } if (mapTime != lastMapTime) { // position has changed rSpeed.add(p.getSpeed()); float perCent = 1.0f; if (mapTime > videoTime + 250) { perCent = ((float) videoTime / mapTime); // map too fast // System.out.println("map too fast, speed up video"); if (perCent > lastCent) { // System.out.println("rate of change decreasing"); diff -= 0.01f; } else { // System.out.println("rate of change increasing"); // rate of change is increasing if (perCent < 0.9 || perCent > 1.1) { diff += 0.06f; } else { diff += 0.03f; } } } else if (videoTime > mapTime + 250) { // video too fast perCent = ((float) mapTime / videoTime); // System.out.println("video too fast, speed up map"); if (perCent < lastCent) { // System.out.println("rate of change increasing"); if (perCent < 0.9 || perCent > 1.1) { // 10% out, use bigger changes diff -= 0.06f; } else { diff -= 0.03f; } } else { // rate of change is decreasing diff += 0.01f; // System.out.println("rate of change decreasing"); } } if (diff > 1.25) { diff = 1.25f; } else if (diff < 0.8) { diff = 0.8f; } lastMapTime = mapTime; logger.debug(String .format("Map Speed %.2f, Smoothed Map Speed %.2f, Turbo Speed %.2f MapTime %d VideoTime %d, perCent %.3f, lastCent %.3f", p.getSpeed(), rSpeed.getAverage(), t.getSpeedKMH(), mapTime, videoTime, perCent, lastCent)); lastCent = perCent; } double rate = 1.0; if (rSpeed.getAverage() > 10.0) { rate = t.getSpeedKMH() / rSpeed.getAverage(); } rate *= diff; BigDecimal bd = new BigDecimal(rate).setScale(2, RoundingMode.HALF_UP); if (lastRate != bd.floatValue()) { lastRate = bd.floatValue(); rRate.add(lastRate); lastRate = (float) rRate.getAverage(); logger.debug(String.format( "Position %f, Diff %.2f Rate %.2f Video-rate %.2f ", mPlayer.getPosition(), diff, lastRate, mPlayer.getRate())); mPlayer.setRate(lastRate); } } /* * @Override public void stateChanged(ChangeEvent e) { if (!videoLoaded || * gpxData == null) { return; } * * Telemetry t = (Telemetry) e.getSource(); */ @Override public void callback(Messages message, Object o) { switch (message) { case SPEED: if (grabber == null && UserPreferences.INSTANCE.isScreenshot()) { (grabber = new ScreenGrabber(this, 300)).start(); } if (videoLoaded && routeData != null) { Telemetry t = (Telemetry) o; setSpeed(t); } break; case STOP: if (mPlayer != null) { if (mPlayer.isPlaying()) { logger.debug("Pausing video player"); mPlayer.pause(); } } if (grabber != null) { grabber.shutdown(); grabber = null; } break; case START: break; case STARTPOS: double startDistance = (Double) o; if (routeData == null) { return; } Point p = routeData.getAbsolutePoint(startDistance); if (p != null) { mapStartTime = p.getTime(); if (mapStartTime > 0 && len > 0) { float pos = (float) mapStartTime / len; mPlayer.setPosition(pos); } } break; case CLOSE: // by default add to telemetry frame remove(odo); mainFrame.add(odo, "cell 0 2, grow"); Rectangle r = getBounds(); UserPreferences.INSTANCE.setVideoBounds(r); // revalidate(); // mainFrame.revalidate(); revalidate(this); revalidate(mainFrame); setVisible(false); MessageBus.INSTANCE.unregister(Messages.SPEED, this); mapStartTime = 0; if (routeData != null) { routeData.close(); len = 0; } if (grabber != null) { grabber.shutdown(); grabber = null; } break; case GPXLOAD: routeData = (RouteReader) o; startTime = 0; if (routeData.routeType() == RouteReader.SLOPE) { // smooth GPX type courses rSpeed = new Rolling(10); } else { rSpeed = new Rolling(1); } rRate = new Rolling(5); String videoFile = routeData.getFilename(); videoLoaded = false; String[] fileTypes = new String[] { ".avi", ".mp4", ".flv" }; for (String ext : fileTypes) { if ((new File(videoFile + ext)).exists()) { mainFrame.remove(odo); mainFrame.repaint(); mPlayer.enableOverlay(false); mPlayer.prepareMedia(videoFile + ext); long DurationInSeconds;// gives us video time mPlayer.parseMedia(); DurationInSeconds = (mPlayer.getMediaMeta().getLength()); // System.out.println("duration " + DurationInSeconds); add(odo, java.awt.BorderLayout.SOUTH); videoLoaded = true; // revalidate(); Java 1.7 code revalidate(this); setVisible(true); MessageBus.INSTANCE.register(Messages.SPEED, this); fps = mPlayer.getFps(); len = mPlayer.getLength(); // System.out.println("fps " + fps + " len " + len); break; } } if (videoLoaded == false) { remove(odo); // mainFrame.revalidate(); Java 1.7 code revalidate(mainFrame); mainFrame.add(odo, "cell 0 2, grow"); // revalidate(); // mainFrame.revalidate(); revalidate(this); revalidate(mainFrame); // mPlayer = null; setVisible(false); MessageBus.INSTANCE.unregister(Messages.SPEED, this); } break; } } private void revalidate(JFrame frame) { // frame.invalidate(); frame.validate(); } /** * Helper class to take periodic screen shots */ public class ScreenGrabber extends Thread { JFrame frame; long seconds; private String userDir; private volatile boolean done = false; ScreenGrabber(JFrame frame, long seconds) { this.seconds = seconds; this.frame = frame; userDir = UserPreferences.INSTANCE.getUserDataDirectory(); } public void run() { while (!done) { try { Thread.sleep(seconds * 1000); } catch (InterruptedException e) { } screenshot(); imageCount++; } } public void shutdown() { done = true; } /** * take a screen shot */ void screenshot() { try { Robot robot = new Robot(); // Capture the screen shot of the area of the screen defined by // the rectangle BufferedImage bi = robot.createScreenCapture(new Rectangle( frame.getX(), frame.getY(), frame.getWidth(), frame.getHeight())); ImageIO.write(bi, "png", new File(userDir + "/screen-" + imageCount + ".png")); } catch (Exception e) { logger.error("Exception to write image " + e.getLocalizedMessage()); } } } }