// Near Infinity - An Infinity Engine Browser and Editor // Copyright (C) 2001 - 2005 Jon Olav Hauglid // See LICENSE.txt for license information package org.infinity.resource.video; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.image.BufferedImage; import java.awt.image.DataBuffer; import java.awt.image.DataBufferInt; import java.io.File; import java.io.IOException; import java.nio.ByteOrder; import java.nio.file.Files; import java.nio.file.Path; import java.util.LinkedList; import javax.swing.BorderFactory; import javax.swing.BoxLayout; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.ProgressMonitor; import javax.swing.SwingConstants; import org.infinity.NearInfinity; import org.infinity.gui.ButtonPanel; import org.infinity.gui.ButtonPopupMenu; import org.infinity.gui.WindowBlocker; import org.infinity.icon.Icons; import org.infinity.resource.Closeable; import org.infinity.resource.Profile; import org.infinity.resource.Resource; import org.infinity.resource.ResourceFactory; import org.infinity.resource.ViewableContainer; import org.infinity.resource.key.ResourceEntry; import org.infinity.search.ReferenceSearcher; import org.monte.media.AudioFormatKeys; import org.monte.media.Format; import org.monte.media.FormatKeys; import org.monte.media.VideoFormatKeys; import org.monte.media.avi.AVIWriter; import org.monte.media.math.Rational; public class MveResource implements Resource, ActionListener, ItemListener, Closeable, Runnable { private static final int VIDEO_BUFFERS = 3; private static final ButtonPanel.Control CtrlPlay = ButtonPanel.Control.CUSTOM_1; private static final ButtonPanel.Control CtrlPause = ButtonPanel.Control.CUSTOM_2; private static final ButtonPanel.Control CtrlStop = ButtonPanel.Control.CUSTOM_3; private static boolean isZoom = true; private static boolean isFilter = true; private final ResourceEntry entry; private final ButtonPanel buttonPanel = new ButtonPanel(); private MveDecoder decoder; private ImageRenderer renderer; private MvePlayer player; private JMenuItem miExport, miExportAvi; private JPanel panel; private JCheckBox cbZoom, cbFilter; public MveResource(ResourceEntry entry) throws Exception { this.entry = entry; player = new MvePlayer(); try { decoder = new MveDecoder(entry); if (!decoder.isOpen()) { decoder.close(); throw new Exception(""); } } catch (Exception e) { decoder = null; e.printStackTrace(); JOptionPane.showMessageDialog(NearInfinity.getInstance(), "Error opening " + entry, "Error", JOptionPane.ERROR_MESSAGE); } } //--------------------- Begin Interface ActionListener --------------------- @Override public void actionPerformed(ActionEvent event) { if (event.getSource() == buttonPanel.getControlByType(ButtonPanel.Control.FIND_REFERENCES)) { new ReferenceSearcher(entry, panel.getTopLevelAncestor()); } else if (miExport == event.getSource()) { ResourceFactory.exportResource(entry, panel.getTopLevelAncestor()); } else if (miExportAvi == event.getSource()) { new Thread(new Runnable() { @Override public void run() { exportAsAvi(entry, (Window)panel.getTopLevelAncestor()); } }).start(); } else if (buttonPanel.getControlByType(CtrlPlay) == event.getSource()) { if (player.isStopped()) { new Thread(this).start(); } else { if (player.isPaused()) { player.continuePlay(); buttonPanel.getControlByType(CtrlPlay).setEnabled(player.isPaused()); buttonPanel.getControlByType(CtrlPause).setEnabled(!player.isPaused()); } } } else if (buttonPanel.getControlByType(CtrlPause) == event.getSource()) { if (!player.isStopped()) { if (!player.isPaused()) { player.pausePlay(); buttonPanel.getControlByType(CtrlPlay).setEnabled(player.isPaused()); buttonPanel.getControlByType(CtrlPause).setEnabled(!player.isPaused()); } } } else if (buttonPanel.getControlByType(CtrlStop) == event.getSource()) { player.stopPlay(); buttonPanel.getControlByType(CtrlStop).setEnabled(false); buttonPanel.getControlByType(CtrlPause).setEnabled(false); buttonPanel.getControlByType(CtrlPlay).setEnabled(true); } } //--------------------- End Interface ActionListener --------------------- //--------------------- Begin Interface ItemListener --------------------- @Override public void itemStateChanged(ItemEvent event) { if (event.getSource() == cbZoom) { if (renderer != null) { isZoom = cbZoom.isSelected(); renderer.setScalingEnabled(isZoom); } } else if (event.getSource() == cbFilter) { if (renderer != null) { isFilter = cbFilter.isSelected(); Object filter = isFilter ? ImageRenderer.TYPE_BILINEAR : ImageRenderer.TYPE_NEAREST_NEIGHBOR; renderer.setInterpolationType(filter); } } } //--------------------- End Interface ItemListener --------------------- //--------------------- Begin Interface Resource --------------------- @Override public ResourceEntry getResourceEntry() { return entry; } //--------------------- End Interface Resource --------------------- //--------------------- Begin Interface Closeable --------------------- @Override public void close() throws Exception { if (player != null) { player.stopPlay(); } if (decoder != null) { decoder.close(); decoder = null; } } //--------------------- End Interface Closeable --------------------- //--------------------- Begin Interface Runnable --------------------- @Override public void run() { if (!decoder.isOpen()) { try { decoder.open(entry); } catch (Exception e) { e.printStackTrace(); JOptionPane.showMessageDialog(panel, "Error starting video playback", "Error", JOptionPane.ERROR_MESSAGE); return; } } buttonPanel.getControlByType(CtrlPlay).setEnabled(false); buttonPanel.getControlByType(CtrlPause).setEnabled(true); buttonPanel.getControlByType(CtrlStop).setEnabled(true); try { renderer.clearBuffers(); player.play(renderer, decoder); } catch (Exception e) { player.stopPlay(); e.printStackTrace(); JOptionPane.showMessageDialog(panel, "Error during playback", "Error", JOptionPane.ERROR_MESSAGE); } decoder.close(); buttonPanel.getControlByType(CtrlPlay).setEnabled(true); buttonPanel.getControlByType(CtrlPause).setEnabled(false); buttonPanel.getControlByType(CtrlStop).setEnabled(false); } //--------------------- End Interface Runable --------------------- //--------------------- Begin Interface Viewable --------------------- @Override public JComponent makeViewer(ViewableContainer container) { if (decoder != null) { renderer = new ImageRenderer(VIDEO_BUFFERS, decoder.getVideoWidth(), decoder.getVideoHeight()); renderer.setHorizontalAlignment(SwingConstants.CENTER); renderer.setVerticalAlignment(SwingConstants.CENTER); if (isFilter) { renderer.setInterpolationType(ImageRenderer.TYPE_BILINEAR); } else { renderer.setInterpolationType(ImageRenderer.TYPE_NEAREST_NEIGHBOR); } renderer.setAspectRatioEnabled(true); renderer.setScalingEnabled(isZoom); decoder.setVideoOutput(renderer); } else { renderer = new ImageRenderer(); } JScrollPane scroll = new JScrollPane(renderer); scroll.setPreferredSize(new Dimension(renderer.getBufferWidth(), renderer.getBufferHeight())); scroll.getVerticalScrollBar().setUnitIncrement(16); scroll.getHorizontalScrollBar().setUnitIncrement(16); scroll.setBorder(BorderFactory.createLoweredBevelBorder()); cbZoom = new JCheckBox("Zoom video", isZoom); cbZoom.addItemListener(this); cbFilter = new JCheckBox("Enable video filtering", isFilter); cbFilter.addItemListener(this); cbFilter.setToolTipText("Uncheck for better video performance"); JPanel optionsPanel = new JPanel(); BoxLayout bl = new BoxLayout(optionsPanel, BoxLayout.Y_AXIS); optionsPanel.setLayout(bl); optionsPanel.add(cbZoom); optionsPanel.add(cbFilter); JButton bPlay = new JButton("Play", Icons.getIcon(Icons.ICON_PLAY_16)); bPlay.addActionListener(this); bPlay.setEnabled(decoder != null); JButton bPause = new JButton("Pause", Icons.getIcon(Icons.ICON_PAUSE_16)); bPause.addActionListener(this); bPause.setEnabled(false); JButton bStop = new JButton("Stop", Icons.getIcon(Icons.ICON_STOP_16)); bStop.addActionListener(this); bStop.setEnabled(false); miExport = new JMenuItem("as MVE"); miExport.addActionListener(this); miExportAvi = new JMenuItem("as AVI"); miExportAvi.addActionListener(this); ButtonPopupMenu bpmExport = (ButtonPopupMenu)ButtonPanel.createControl(ButtonPanel.Control.EXPORT_MENU); bpmExport.setMenuItems(new JMenuItem[]{miExport, miExportAvi}); buttonPanel.addControl(bPlay, CtrlPlay); buttonPanel.addControl(bPause, CtrlPause); buttonPanel.addControl(bStop, CtrlStop); ((JButton)buttonPanel.addControl(ButtonPanel.Control.FIND_REFERENCES)).addActionListener(this); buttonPanel.addControl(bpmExport, ButtonPanel.Control.EXPORT_MENU); buttonPanel.addControl(optionsPanel); panel = new JPanel(); panel.setLayout(new BorderLayout()); panel.add(scroll, BorderLayout.CENTER); panel.add(buttonPanel, BorderLayout.SOUTH); return panel; } //--------------------- End Interface Viewable --------------------- private static void exportAsAvi(ResourceEntry inEntry, Window parent) { if (inEntry != null) { JFileChooser fc = new JFileChooser(Profile.getGameRoot().toFile()); fc.setDialogTitle("Export MVE as AVI"); String name = inEntry.getResourceName(); if (name.lastIndexOf('.') > 0) { name = name.substring(0, name.lastIndexOf('.')) + ".avi"; } else { name = name + ".avi"; } fc.setSelectedFile(new File(fc.getCurrentDirectory(), name)); fc.setDialogType(JFileChooser.SAVE_DIALOG); fc.setFileSelectionMode(JFileChooser.FILES_ONLY); if (fc.showSaveDialog(parent) == JFileChooser.APPROVE_OPTION) { boolean cancelled = false; if (fc.getSelectedFile().isFile()) { final String[] options = {"Overwrite", "Cancel"}; final String msg = fc.getSelectedFile().toString() + " exists. Overwrite?"; final String title = "Export MVE to AVI"; int ret = JOptionPane.showOptionDialog(parent, msg, title, JOptionPane.YES_NO_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); cancelled = (ret != JOptionPane.YES_OPTION); } if (!cancelled) { try { WindowBlocker.blockWindow(parent, true); convertAvi(inEntry, fc.getSelectedFile().toPath(), parent, false); } finally { WindowBlocker.blockWindow(parent, false); } } } fc = null; } } public static boolean convertAvi(ResourceEntry inEntry, Path outFile, Window parent, boolean silent) { if (inEntry == null || outFile == null) { if (!silent) { JOptionPane.showMessageDialog(parent, "No input or output file specified.", "Error", JOptionPane.ERROR_MESSAGE); } return false; } Format videoFormat = new Format(VideoFormatKeys.EncodingKey, VideoFormatKeys.ENCODING_AVI_MJPG, VideoFormatKeys.DepthKey, 24, VideoFormatKeys.QualityKey, 1.0f); try { MveDecoder decoder = null; ProgressMonitor pm = null; AVIWriter writer = null; try { if (!silent) { pm = new ProgressMonitor(parent, "Converting MVE to AVI...", "Initializing", 0, 2); pm.setMillisToDecideToPopup(0); pm.setMillisToPopup(0); } decoder = new MveDecoder(inEntry); decoder.setDefaultAudioOutput(new AudioQueue()); // prebuffering audio and searching for first video frame LinkedList<byte[]> audioQueue = new LinkedList<byte[]>(); while (decoder.hasNextFrame()) { decoder.processNextFrame(); if (!decoder.frameHasVideo()) { if (decoder.frameHasAudio()) { byte[] buffer = decoder.getAudioOutput(0).getNextData(); if (buffer != null) { audioQueue.add(buffer); } } } else { break; } } writer = new AVIWriter(outFile.toFile()); // initializing video track int rate = 1000000; int scale = decoder.getFrameDelay(); if (scale == 0) { scale = 66728; } // assuming default frame rate final int[] prim = { 29, 23, 19, 17, 13, 11, 7, 5, 3, 2 }; boolean divisible; do { divisible = false; for (int i = 0; i < prim.length; i++) { if (rate % prim[i] == 0 && scale % prim[i] == 0) { divisible = true; rate /= prim[i]; scale /= prim[i]; } } } while (divisible); int width = decoder.getVideoWidth(); int height = decoder.getVideoHeight(); decoder.setVideoOutput(new BasicVideoBuffer(1, width, height, false)); videoFormat = videoFormat.prepend(VideoFormatKeys.MediaTypeKey, FormatKeys.MediaType.VIDEO, VideoFormatKeys.FrameRateKey, new Rational(rate, scale), VideoFormatKeys.WidthKey, width, VideoFormatKeys.HeightKey, height); int trackVideo = writer.addTrack(videoFormat); // initializing audio track Format audioFormat = null; int channels = decoder.getAudioFormat().getChannels(); int sampleRate = (int)decoder.getAudioFormat().getSampleRate(); int sampleBits = decoder.getAudioFormat().getSampleSizeInBits(); int frameSize = decoder.getAudioFormat().getFrameSize(); audioFormat = new Format(AudioFormatKeys.EncodingKey, AudioFormatKeys.ENCODING_PCM_SIGNED, AudioFormatKeys.ByteOrderKey, ByteOrder.LITTLE_ENDIAN, AudioFormatKeys.ChannelsKey, channels, AudioFormatKeys.SampleRateKey, new Rational(sampleRate), AudioFormatKeys.SampleSizeInBitsKey, sampleBits, AudioFormatKeys.FrameSizeKey, frameSize, AudioFormatKeys.SignedKey, true); int trackAudio = writer.addTrack(audioFormat); // default audio buffer for one frame int bufferSize = (int)Math.ceil((double)(sampleRate)*(double)scale/(double)rate) * frameSize; byte[] defaultBuffer = new byte[bufferSize]; if (!silent) { pm.setProgress(1); } int frameIdx = 0; // writing prebuffered audio data first while (!audioQueue.isEmpty()) { byte[] buffer = audioQueue.pollFirst(); writer.writeSample(trackAudio, buffer, 0, buffer.length, true); } // writing regular frame data do { if (!silent && frameIdx % 10 == 0) { pm.setNote(String.format("Processing frame %1$d", frameIdx)); } if (decoder.frameHasVideo()) { BufferedImage image = (BufferedImage)decoder.getVideoOutput().frontBuffer(); adjustColorSpace(image); writer.write(trackVideo, image, 1); image = null; } byte[] buffer = decoder.getAudioOutput(0).getNextData(); if (buffer == null) { buffer = defaultBuffer; } writer.writeSample(trackAudio, buffer, 0, buffer.length, true); frameIdx++; if (!silent && pm.isCanceled()) { if (writer != null) { writer.close(); writer = null; } if (Files.isRegularFile(outFile)) { try { Files.delete(outFile); } catch (IOException e) { e.printStackTrace(); } } JOptionPane.showMessageDialog(parent, "Conversion has been cancelled.", "Information", JOptionPane.INFORMATION_MESSAGE); return true; } } while (decoder.processNextFrame()); if (!silent) { pm.setProgress(2); } } finally { if (decoder != null) { decoder.close(); decoder = null; } if (writer != null) { writer.close(); writer = null; } if (pm != null) { pm.close(); pm = null; } } if (!silent) { JOptionPane.showMessageDialog(parent, "Resource has been converted successfully: " + inEntry, "Information", JOptionPane.INFORMATION_MESSAGE); } return true; } catch (Exception e) { e.printStackTrace(); } if (!silent) { JOptionPane.showMessageDialog(parent, "Error while exporting " + inEntry + " as AVI file.", "Error", JOptionPane.ERROR_MESSAGE); } return false; } // Reduces color range from [0, 255] to [16, 235] to conform to CCIR-601 standard. private static void adjustColorSpace(BufferedImage image) { if (image != null) { if (image.getRaster().getDataBuffer().getDataType() == DataBuffer.TYPE_INT) { // true color image int[] data = ((DataBufferInt)image.getRaster().getDataBuffer()).getData(); for (int i = 0; i < data.length; i++) { int b = data[i] & 0xff; b = (16 + ((b * 220) >>> 8)) & 0xff; int g = (data[i] >>> 8) & 0xff; g = (16 + ((g * 220) >>> 8)) & 0xff; int r = (data[i] >>> 16) & 0xff; r = (16 + ((r * 220) >>> 8)) & 0xff; data[i] = (data[i] & 0xff000000) | (r << 16) | (g << 8) | b; } } } } }