/**
* Created : Mar 25, 2012
*
* @author pquiring
*/
import java.io.*;
import java.util.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.table.*;
import javaforce.*;
import javaforce.jbus.*;
import javaforce.jni.*;
import javaforce.media.*;
public class MainPanel extends javax.swing.JPanel implements ActionListener {
/**
* Creates new form MainPanel
*/
public MainPanel() {
initComponents();
playIcon = new javax.swing.ImageIcon(getClass().getResource("/play.png"));
pauseIcon = new javax.swing.ImageIcon(getClass().getResource("/pause.png"));
model = (DefaultTableModel)table.getModel();
if (!JF.isWindows()) {
jbusClient = new JBusClient(null, null); //send only
jbusClient.start();
}
loadIcons();
xml.root.setName("jMedia");
library = xml.addTag(xml.root, "Library", "", "");
music = xml.addTag(library, "Music", "", "");
music.isLeaf = true; //avoid showing files in JTree
video = xml.addTag(library, "Video", "", "");
video.isLeaf = true; //avoid showing files in JTree
// playlists = xml.addTag(xml.root, "Play Lists", "", "");
if (!JF.isWindows()) {
media = xml.addTag(xml.root, "Media", "", "");
}
addLibrary(new File(JF.getUserPath() + "/Music"));
addLibrary(new File(JF.getUserPath() + "/Videos"));
showAll();
This = this;
}
/**
* This method is called from within the constructor to initialize the form.
* WARNING: Do NOT modify this code. The content of this method is always
* regenerated by the Form Editor.
*/
@SuppressWarnings("unchecked")
// <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
private void initComponents() {
jSplitPane1 = new javax.swing.JSplitPane();
jScrollPane1 = new javax.swing.JScrollPane();
tree = new javax.swing.JTree();
jScrollPane2 = new javax.swing.JScrollPane();
table = new javax.swing.JTable();
jToolBar1 = new javax.swing.JToolBar();
play = new javax.swing.JButton();
prev = new javax.swing.JButton();
next = new javax.swing.JButton();
jSeparator1 = new javax.swing.JToolBar.Separator();
repeat = new javax.swing.JToggleButton();
random = new javax.swing.JToggleButton();
jSeparator2 = new javax.swing.JToolBar.Separator();
extract = new javax.swing.JButton();
jSeparator3 = new javax.swing.JToolBar.Separator();
addFolder = new javax.swing.JButton();
time = new javax.swing.JSlider();
jSplitPane1.setResizeWeight(0.5);
tree.setModel(xml.getTreeModel());
tree.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() {
public void valueChanged(javax.swing.event.TreeSelectionEvent evt) {
treeValueChanged(evt);
}
});
jScrollPane1.setViewportView(tree);
jSplitPane1.setLeftComponent(jScrollPane1);
table.setModel(new javax.swing.table.DefaultTableModel(
new Object [][] {
},
new String [] {
"Status", "Select", "Track #", "Track Name", "Artist", "Album", "Length"
}
) {
Class[] types = new Class [] {
java.lang.Object.class, java.lang.Boolean.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class, java.lang.String.class
};
boolean[] canEdit = new boolean [] {
false, true, false, true, true, true, false
};
public Class getColumnClass(int columnIndex) {
return types [columnIndex];
}
public boolean isCellEditable(int rowIndex, int columnIndex) {
return canEdit [columnIndex];
}
});
table.setSelectionMode(javax.swing.ListSelectionModel.SINGLE_SELECTION);
table.setShowVerticalLines(false);
table.addMouseListener(new java.awt.event.MouseAdapter() {
public void mouseClicked(java.awt.event.MouseEvent evt) {
tableMouseClicked(evt);
}
});
jScrollPane2.setViewportView(table);
if (table.getColumnModel().getColumnCount() > 0) {
table.getColumnModel().getColumn(0).setPreferredWidth(20);
table.getColumnModel().getColumn(0).setCellRenderer(statusCellRenderer);
table.getColumnModel().getColumn(1).setPreferredWidth(20);
table.getColumnModel().getColumn(2).setPreferredWidth(20);
}
jSplitPane1.setRightComponent(jScrollPane2);
jToolBar1.setFloatable(false);
jToolBar1.setRollover(true);
play.setIcon(new javax.swing.ImageIcon(getClass().getResource("/play.png"))); // NOI18N
play.setFocusable(false);
play.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
play.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
play.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
playActionPerformed(evt);
}
});
jToolBar1.add(play);
prev.setIcon(new javax.swing.ImageIcon(getClass().getResource("/prev.png"))); // NOI18N
prev.setFocusable(false);
prev.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
prev.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
prev.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
prevActionPerformed(evt);
}
});
jToolBar1.add(prev);
next.setIcon(new javax.swing.ImageIcon(getClass().getResource("/next.png"))); // NOI18N
next.setFocusable(false);
next.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
next.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
next.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
nextActionPerformed(evt);
}
});
jToolBar1.add(next);
jToolBar1.add(jSeparator1);
repeat.setIcon(new javax.swing.ImageIcon(getClass().getResource("/repeat.png"))); // NOI18N
repeat.setFocusable(false);
repeat.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
repeat.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
jToolBar1.add(repeat);
random.setIcon(new javax.swing.ImageIcon(getClass().getResource("/random.png"))); // NOI18N
random.setFocusable(false);
random.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
random.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
jToolBar1.add(random);
jSeparator2.setEnabled(false);
jToolBar1.add(jSeparator2);
extract.setText("Extract");
extract.setEnabled(false);
extract.setFocusable(false);
extract.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
extract.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
extract.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
extractActionPerformed(evt);
}
});
jToolBar1.add(extract);
jToolBar1.add(jSeparator3);
addFolder.setText("Add Folder");
addFolder.setFocusable(false);
addFolder.setHorizontalTextPosition(javax.swing.SwingConstants.CENTER);
addFolder.setVerticalTextPosition(javax.swing.SwingConstants.BOTTOM);
addFolder.addActionListener(new java.awt.event.ActionListener() {
public void actionPerformed(java.awt.event.ActionEvent evt) {
addFolderActionPerformed(evt);
}
});
jToolBar1.add(addFolder);
time.setValue(0);
time.addChangeListener(new javax.swing.event.ChangeListener() {
public void stateChanged(javax.swing.event.ChangeEvent evt) {
timeStateChanged(evt);
}
});
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this);
this.setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addComponent(jSplitPane1)
.addComponent(jToolBar1, javax.swing.GroupLayout.DEFAULT_SIZE, 565, Short.MAX_VALUE)
.addComponent(time, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
.addComponent(jToolBar1, javax.swing.GroupLayout.PREFERRED_SIZE, 25, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(time, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
.addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
.addComponent(jSplitPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 657, Short.MAX_VALUE))
);
}// </editor-fold>//GEN-END:initComponents
private void treeValueChanged(javax.swing.event.TreeSelectionEvent evt) {//GEN-FIRST:event_treeValueChanged
if (ripping) return;
extract.setEnabled(false);
int cnt = tree.getSelectionCount();
if (cnt != 1) return;
XML.XMLTag tag = xml.getTag(tree.getSelectionPath());
if (tag == xml.root) return;
if (media != null && tag == media) {
listDiscs();
} if (media != null && tag.getParent() == media) {
currentIdx = -1; //BUG : status will no longer get updated
listDisc(tag);
} else {
if (tag.getParent() == xml.root) return; //library or playlists
currentIdx = -1; //BUG : status will no longer get updated
listTag(tag);
}
}//GEN-LAST:event_treeValueChanged
private void tableMouseClicked(java.awt.event.MouseEvent evt) {//GEN-FIRST:event_tableMouseClicked
updateToolbar();
if (evt.getClickCount() != 2) return;
playActionPerformed(null);
}//GEN-LAST:event_tableMouseClicked
private void playActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_playActionPerformed
int idx = table.getSelectedRow();
if ((playing) && (idx == currentIdx)) {
if (paused) resume(); else pause();
return;
}
if (playing) stop(true);
if (random.isSelected()) {
makeRandomIdxList();
randomIdx = 0;
if (randomIdx >= randomIdxList.length) return;
idx = randomIdxList[randomIdx];
} else {
idx = table.getSelectedRow();
if (idx == -1) return;
}
play(idx);
}//GEN-LAST:event_playActionPerformed
private void timeStateChanged(javax.swing.event.ChangeEvent evt) {//GEN-FIRST:event_timeStateChanged
seek();
}//GEN-LAST:event_timeStateChanged
private void extractActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_extractActionPerformed
extract();
}//GEN-LAST:event_extractActionPerformed
private void nextActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_nextActionPerformed
next();
}//GEN-LAST:event_nextActionPerformed
private void prevActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_prevActionPerformed
prev();
}//GEN-LAST:event_prevActionPerformed
private void addFolderActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_addFolderActionPerformed
addFolder();
}//GEN-LAST:event_addFolderActionPerformed
// Variables declaration - do not modify//GEN-BEGIN:variables
private javax.swing.JButton addFolder;
private javax.swing.JButton extract;
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JScrollPane jScrollPane2;
private javax.swing.JToolBar.Separator jSeparator1;
private javax.swing.JToolBar.Separator jSeparator2;
private javax.swing.JToolBar.Separator jSeparator3;
private javax.swing.JSplitPane jSplitPane1;
private javax.swing.JToolBar jToolBar1;
private javax.swing.JButton next;
private javax.swing.JButton play;
private javax.swing.JButton prev;
private javax.swing.JToggleButton random;
private javax.swing.JToggleButton repeat;
private javax.swing.JTable table;
private javax.swing.JSlider time;
private javax.swing.JTree tree;
// End of variables declaration//GEN-END:variables
private XML xml = new XML();
private XML.XMLTag library, playlists, media;
private XML.XMLTag music, video; //library sub-tags
private ArrayList<String> tableFiles = new ArrayList<String>();
private MediaDecoder decoder; //ffmpeg decoder
private long frameCount;
private long audioCount;
private final Object countLock = new Object();
private boolean playing, paused, updatingPos, eof, preBuffering;
private int currentIdx = -1;
private VideoPanel videoPanel;
private javax.swing.Timer timer; //to update position
private DefaultTableModel model;
private boolean ripping, abort;
private JFImage icon_playing, icon_paused, icon_ripped, icon_ripping;
private int randomIdx, randomIdxList[];
private StatusCellRenderer statusCellRenderer = new StatusCellRenderer();
private JBusClient jbusClient;
private ReadThread readThread;
private Thread playThread;
private static MainPanel This;
private long fileLength;
private int seekPosition;
private long mediaLength;
private Icon playIcon, pauseIcon;
private class StatusCellRenderer extends javax.swing.table.DefaultTableCellRenderer {
protected void setValue(Object value) {
JFImage image = (JFImage)value; //NOTE:may be null
this.setIcon(image);
this.setHorizontalAlignment(javax.swing.SwingConstants.CENTER);
}
}
private void loadIcons() {
icon_playing = new JFImage();
icon_playing.loadPNG(this.getClass().getClassLoader().getResourceAsStream("playing.png"));
icon_paused = new JFImage();
icon_paused.loadPNG(this.getClass().getClassLoader().getResourceAsStream("paused.png"));
icon_ripped = new JFImage();
icon_ripped.loadPNG(this.getClass().getClassLoader().getResourceAsStream("ripped.png"));
icon_ripping = new JFImage();
icon_ripping.loadPNG(this.getClass().getClassLoader().getResourceAsStream("ripping.png"));
}
private void makeRandomIdxList() {
int length = table.getRowCount();
randomIdxList = new int[length];
boolean used[] = new boolean[length];
Random r = new Random();
for(int a=0;a<length;a++) {
int idx = r.nextInt() % length;
while (used[idx]) {
idx++;
if (idx == length) idx = 0;
}
used[idx] = true;
randomIdxList[a] = idx;
}
}
private boolean isMediaMusic(String fn) {
if (fn.endsWith(".wav")) return true;
if (fn.endsWith(".fla")) return true;
if (fn.endsWith(".mp3")) return true;
if (fn.endsWith(".wma")) return true;
if (fn.endsWith(".oga")) return true;
if (fn.endsWith(".spx")) return true;
return false;
}
private boolean isMediaVideo(String fn) {
if (fn.endsWith(".ogg")) return true;
if (fn.endsWith(".ogv")) return true;
if (fn.endsWith(".avi")) return true;
if (fn.endsWith(".wmv")) return true;
if (fn.endsWith(".mpg")) return true;
if (fn.endsWith(".mp4")) return true;
if (fn.endsWith(".flv")) return true;
if (fn.endsWith(".mpeg")) return true;
if (fn.endsWith(".3gp")) return true;
if (fn.endsWith(".h263")) return true;
if (fn.endsWith(".h264")) return true;
if (fn.endsWith(".webm")) return true;
if (fn.endsWith(".mov")) return true;
return false;
}
private void addLibrary(File file) {
File files[] = file.listFiles();
if (files == null) return;
for(int a=0;a<files.length;a++) {
if (files[a].isDirectory()) {
addLibrary(files[a]);
} else {
if (isMediaMusic(files[a].getName().toLowerCase())) {
xml.addTag(music, files[a].getName(), "", files[a].getAbsolutePath());
}
if (isMediaVideo(files[a].getName().toLowerCase())) {
xml.addTag(video, files[a].getName(), "", files[a].getAbsolutePath());
}
}
}
}
private void showAll(XML.XMLTag tag) {
tree.makeVisible(new TreePath(tag.getPath()));
int cnt = tag.getChildCount();
for(int a=0;a<cnt;a++) {
showAll(tag.getChildAt(a));
}
}
private void showAll() {
showAll(xml.root);
}
private void addRow(JLabel icon, boolean select, int track, String name, String artist, String album, String length, String filename) {
model.addRow(new Object[] {icon, select, track, name, artist, album, length});
tableFiles.add(filename);
}
private void clearList() {
tableFiles.clear();
while (model.getRowCount() > 0) model.removeRow(0);
}
private void listTag(XML.XMLTag tag) {
clearList();
int cnt = tag.getChildCount();
int track = 1;
for(int a=0;a<cnt;a++) {
String name = tag.getChildAt(a).name;
int idx = name.lastIndexOf(".");
if (idx != -1) name = name.substring(0, idx);
addRow(null, false, track++, name, "Unknown", "Unknown" ,"?", tag.getChildAt(a).content);
}
}
/** Play a file in current playlist. */
private void play(int idx) {
if (playing) {stop(true); return;}
model.setValueAt(icon_playing, idx, 0);
currentIdx = idx;
playing = true;
readThread = new ReadThread(tableFiles.get(idx));
readThread.start();
play.setIcon(pauseIcon);
timer = new javax.swing.Timer(1000, this);
timer.start();
}
/** Play a file directly. */
public void play(File file) {
if (playing) {stop(true); return;}
currentIdx = -1;
playing = true;
readThread = new ReadThread(file.getAbsolutePath());
readThread.start();
play.setIcon(pauseIcon);
timer = new javax.swing.Timer(1000, this);
timer.start();
}
public synchronized void stop(boolean wait) {
if (!playing) return;
if (decoder == null) return;
if (readThread == null) return;
timer.stop();
timer = null;
playing = false;
if (wait) {
JFLog.log("wait for read thread");
try {readThread.join();} catch (Exception e) {JFLog.log(e);}
JFLog.log("read thread done");
}
play.setIcon(playIcon);
if (currentIdx != -1) model.setValueAt(null, currentIdx, 0);
time.setValue(0);
if (videoPanel != null) videoPanel.time().setValue(0);
}
private void pause() {
if (!playing) return;
if (paused) return;
if (decoder == null) return;
play.setIcon(playIcon);
if (videoPanel != null) videoPanel.play().setIcon(playIcon);
paused = true;
if (currentIdx != -1) model.setValueAt(icon_paused, currentIdx, 0);
}
private void resume() {
if (!paused) return;
play.setIcon(pauseIcon);
if (videoPanel != null) videoPanel.play().setIcon(pauseIcon);
paused = false;
if (currentIdx != -1) model.setValueAt(icon_playing, currentIdx, 0);
}
public void actionPerformed(ActionEvent ae) {
//timer task
try {
if (!playing) return;
if (seekPosition != -1) return; //seeking
long pos;
if (fps > 0) {
//video
pos = (long)(frameCount / decoder.getFrameRate());
} else {
//audio only
pos = audioCount / (44100 * chs);
}
updatingPos = true;
if (mediaLength == 0) {
time.setValue(0);
if (videoPanel != null) videoPanel.time().setValue(0);
} else {
int ipos = (int)((pos * 100) / mediaLength);
// JFLog.log("ipos=" + ipos);
time.setValue(ipos);
if (videoPanel != null) videoPanel.time().setValue(ipos);
}
updatingPos = false;
} catch (Exception e) {
e.printStackTrace();
}
}
private void listDiscs() {
int cnt = media.getChildCount();
for(int a=0;a<cnt;a++) {
xml.deleteTag(media.getChildAt(a));
}
File file = new File("/media");
if (!file.exists()) return;
if (!file.isDirectory()) return;
File folders[] = file.listFiles();
if (folders == null) return;
cnt = folders.length;
int discNo = 1;
for(int a=0;a<cnt;a++) {
if (folders[a].getName().startsWith("CDROM")) {
xml.addTag(media, "Disc " + discNo++, "", folders[a].getName());
}
}
showAll();
}
private String seconds2String(long time) {
long hrs = time / 60 / 60;
time -= (hrs * 60 * 60);
long mins = time / 60;
time -= (mins * 60);
long secs = time;
if (hrs > 0) {
return "" + hrs + ":" + mins + ":" + secs;
}
return "" + mins + ":" + secs;
}
private void listDisc(XML.XMLTag tag) {
clearList();
File file = new File("/media/" + tag.content);
File tracks[] = file.listFiles();
int track = 1;
String artist = "Unknown";
String album = "Unknown";
GetNamesDialog dialog = new GetNamesDialog(MediaApp.frame, true);
dialog.setVisible(true);
if (dialog.accepted) {
artist = dialog.getArtist();
album = dialog.getAlbum();
}
for(int a=0;a<tracks.length;a++) {
if (!tracks[a].getName().endsWith(".wav")) continue;
long length = tracks[a].length() / 176800; //seconds
String lenStr = seconds2String(length);
String name = tracks[a].getName();
int idx = name.lastIndexOf(".");
if (idx != -1) name = name.substring(0, idx);
addRow(null, false, track, name, artist, album, lenStr, tracks[a].getAbsolutePath());
track++;
}
if (track > 1) extract.setEnabled(true);
}
private boolean copyFile(File src, File dst) {
try {
FileInputStream fis = new FileInputStream(src);
File dstParent = dst.getParentFile();
dstParent.mkdirs();
FileOutputStream fos = new FileOutputStream(dst);
int length = fis.available();
int copied = 0;
byte buf[] = new byte[4096];
while (copied < length) {
if (abort) break;
int read = fis.read(buf);
if (read == -1) throw new Exception("file io error");
if (read == 0) continue;
fos.write(buf, 0, read);
copied += read;
}
fis.close();
fos.close();
if (abort) {
dst.delete();
return false;
}
return true;
} catch (Exception e) {
JFLog.log(e);
return false;
}
}
private void extract() {
if (ripping) {abort = true; return;}
ripping = true;
extract.setText("Abort");
new Thread() {
public void run() {
String path = JF.getUserPath() + "/Music/";
File file = new File(path);
file.mkdirs();
int cnt = model.getRowCount();
boolean sel[] = new boolean[cnt];
String names[] = new String[cnt];
String files[] = new String[cnt];
String artist[] = new String[cnt];
String album[] = new String[cnt];
for(int a=0;a<cnt;a++) {
sel[a] = (Boolean)model.getValueAt(a, 1);
names[a] = (String)model.getValueAt(a, 3);
artist[a] = (String)model.getValueAt(a, 4);
album[a] = (String)model.getValueAt(a, 5);
files[a] = (String)tableFiles.get(a);
}
for(int a=0;a<cnt;a++) {
if (abort) break;
if (!sel[a]) continue;
model.setValueAt(icon_ripping, a, 0);
if (!copyFile(
new File(files[a]),
new File(path + artist[a] + "/" + album[a] + "/" + names[a] + ".wav"))) {
JF.showError("Error", "Extract failed for track " + (a+1));
break;
}
model.setValueAt(icon_ripped, a, 0);
}
ripping = false;
extract.setText("Extract");
}
}.start();
}
private void next() {
if (playing) stop(true);
int nextIdx = currentIdx + 1;
if (random.isSelected()) {
if (randomIdx+1 >= randomIdxList.length) return;
randomIdx++;
nextIdx = randomIdxList[randomIdx];
}
if (nextIdx >= model.getRowCount()) return;
play(nextIdx);
}
private void prev() {
if (playing) stop(true);
int nextIdx = currentIdx - 1;
if (random.isSelected()) {
if (randomIdx == 0) return;
randomIdx--;
nextIdx = randomIdxList[randomIdx];
}
if (nextIdx < 0) return;
play(nextIdx);
}
private void addFolder() {
JFileChooser chooser = new JFileChooser();
chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
chooser.setMultiSelectionEnabled(false);
chooser.setCurrentDirectory(new File(JF.getUserPath()));
if (chooser.showOpenDialog(this) != JFileChooser.APPROVE_OPTION) return;
currentIdx = -1; //BUG : status will no longer get updated
addLibrary(chooser.getSelectedFile());
}
public void updateToolbar() {
int idx = table.getSelectedRow();
if (idx == -1) return;
if ((idx == currentIdx) && (playing)) play.setIcon(pauseIcon); else play.setIcon(playIcon);
}
public void log(String msg) {
JFLog.log("" + System.currentTimeMillis() + ":" + msg);
}
private AudioBuffer audio_buffer;
private VideoBuffer video_buffer;
final int audio_bufsiz = 1024;
final int chs = 2; //currently all formats are converted to stereo
float fps;
int width, height;
int new_width, new_height;
boolean resizeVideo;
Object sizeLock = new Object();
//buffer size in seconds
//too small can cause problems
//too large causes resizes to take a long time to take effect
//the problem is that some video files are not interlaced very well
final int buffer_seconds = 4;
final int pre_buffer_seconds = 2;
public class ReadThread extends Thread implements MediaIO {
private String file;
int m_in; //input data
int m_out[] = new int[4]; //output data
RandomAccessFile raf;
long fileRead = 0;
byte fileBuf[] = new byte[64*1024];
public ReadThread(String file) {
this.file = file;
}
public void run() {
frameCount = 0;
audioCount = 0;
seekPosition = -1;
audio_buffer = new AudioBuffer(44100, chs, buffer_seconds);
video_buffer = null;
width = -1;
height = -1;
resizeVideo = false;
eof = false;
preBuffering = true;
try {
decoder = new MediaDecoder();
if (decoder == null) throw new Exception("Unable to allocate decoder");
raf = new RandomAccessFile(file, "r");
System.out.println("file=" + file);
if (!decoder.start(this, -1, -1, chs, 44100, true)) throw new Exception("Unable to start decoder");
fileLength = new File(file).length();
String err = null;
mediaLength = decoder.getDuration();
JFLog.log("Duration=" + mediaLength);
fps = decoder.getFrameRate();
JFLog.log("FPS=" + fps);
if (fps > 0) {
videoPanel = new VideoPanel();
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
setPanel(videoPanel);
}
});
videoPanel.start();
width = getWidth();
height = getHeight();
decoder.resize(width, height);
video_buffer = new VideoBuffer(width, height, buffer_seconds * (int)fps);
playThread = new PlayAudioVideoThread();
playThread.start();
} else {
playThread = new PlayAudioOnlyThread();
playThread.start();
}
JFLog.log("Video Bit Rate=" + decoder.getVideoBitRate());
JFLog.log("Audio Bit Rate=" + decoder.getAudioBitRate());
int avBitRate = decoder.getVideoBitRate() + decoder.getAudioBitRate();
if (avBitRate == 0) avBitRate = 64000;
if (mediaLength <= 0) {
mediaLength = fileLength / (avBitRate / 8);
JFLog.log("Calculated Duration=" + mediaLength);
}
while (playing && !eof) {
if (paused) {
JF.sleep(100); //TODO:use a lock with wait() and notify() instead
preBuffering = true; //pre buffer again after unpause
continue;
}
if ((video_buffer != null && video_buffer.size() >= fps * (buffer_seconds-1)) || (audio_buffer.size() > (44100 * chs * (buffer_seconds-1)))) {
preBuffering = false; //in case we don't even have pre_buffer_seconds of video frames
int sleep;
if (fps > 0) {
sleep = 1000 / (int)fps;
// JFLog.log("video sleeping:" + sleep + ":" + fps);
} else {
sleep = 1000 / ((44100 * chs) / (audio_bufsiz));
// JFLog.log("audio sleeping:" + sleep);
}
JF.sleep(sleep);
continue;
}
if (seekPosition != -1) {
long seekTime = (mediaLength / 100) * seekPosition;
if (!decoder.seek(seekTime)) {
JFLog.log("Seek failed");
} else {
synchronized(countLock) {
frameCount = (long)(seekTime * decoder.getFrameRate());
audioCount = seekTime * 44100 * chs;
}
}
seekPosition = -1;
video_buffer.clear();
audio_buffer.clear();
}
if (resizeVideo) {
synchronized(sizeLock) {
width = new_width;
height = new_height;
decoder.resize(width, height);
resizeVideo = false;
}
}
switch (decoder.read()) {
case MediaCoder.AUDIO_FRAME: //audio packet read
short audio[] = decoder.getAudio();
audio_buffer.add(audio, 0, audio.length);
break;
case MediaCoder.VIDEO_FRAME: //video packet read
int video[] = decoder.getVideo();
JFImage img = video_buffer.getNewFrame();
if (img != null) {
if ((img.getWidth() != width) || (img.getHeight() != height)) {
img.setSize(width, height);
}
img.putPixels(video, 0, 0, width, height, 0);
video_buffer.freeNewFrame();
} else {
JFLog.log("Warning : VideoBuffer overflow");
}
break;
case MediaCoder.END_FRAME:
eof = true;
break;
}
}
if (err != null) JF.showError("Error", err);
} catch (Exception e) {
JF.showError("Error", e.toString());
JFLog.log(e);
}
try {
JFLog.log("wait for play thread");
playThread.join();
JFLog.log("play thread done");
} catch (Exception e) {
JFLog.log(e);
}
playThread = null;
audio_buffer = null;
video_buffer = null;
if (videoPanel != null) {
videoPanel.stop();
videoPanel = null;
setPanel(MainPanel.this);
}
if (playing) MainPanel.this.stop(false);
decoder = null;
JFLog.log("read thread exit");
}
public int read(MediaCoder coder, byte data[]) {
int read = 0;
try {
read = raf.read(data, 0, data.length);
} catch (Exception e) {
JFLog.log(e);
return read;
}
if (read == -1) read = 0;
return read;
}
public int write(MediaCoder coder, byte data[]) {
// jfmedia does not create media files
return 0;
}
public long seek(MediaCoder coder, long pos, int how) {
long opos = pos;
try {
switch (how) {
case MediaCoder.SEEK_SET: break; //seek set
case MediaCoder.SEEK_CUR: pos += raf.getFilePointer(); break; //seek cur
case MediaCoder.SEEK_END: pos += raf.length(); break; //seek end
}
raf.seek(pos);
} catch (Exception e) {
JFLog.log(e);
}
return pos;
}
}
public class PlayAudioVideoThread extends Thread {
public void run() {
double frameDelay = 1000.0f / fps;
double samplesPerFrame = (44100.0 * ((double)chs)) / fps;
JFLog.log("samplesPerFrame=" + samplesPerFrame);
double samplesToWrite = 0;
AudioOutput output = new AudioOutput();
output.start(chs, 44100, 16, audio_bufsiz * 2 /*bytes*/, "<default>");
short samples[] = new short[audio_bufsiz];
double current = System.currentTimeMillis();
int skip = 0;
while (playing) {
if (preBuffering) {
//wait till buffers are 50% full before starting
while (!eof && playing && preBuffering) {
if (video_buffer.size() >= (fps * pre_buffer_seconds)) break;
JF.sleep(25);
}
preBuffering = false;
for(int a=0;a<2;a++) output.write(samples); //prime audio output
}
samplesToWrite += samplesPerFrame;
if (eof) {
if ((video_buffer.size() == 0) && (audio_buffer.size() < audio_bufsiz)) break;
}
while (audio_buffer.size() >= audio_bufsiz && samplesToWrite >= audio_bufsiz) {
audio_buffer.get(samples, 0, audio_bufsiz);
output.write(samples);
samplesToWrite -= audio_bufsiz;
synchronized(countLock) { audioCount += audio_bufsiz; };
}
if (video_buffer.size() > 0) {
JFImage img = video_buffer.getNextFrame();
synchronized(countLock) { frameCount++; }
while (skip > 0 && video_buffer.size() > 1) {
if (img == null) break;
skip--;
video_buffer.freeNextFrame();
img = video_buffer.getNextFrame();
synchronized(countLock) { frameCount++; }
}
skip = 0;
if (img != null) {
videoPanel.setImage(img);
video_buffer.freeNextFrame();
}
} else {
JFLog.log("Playback too slow - skipping a frame");
skip++;
}
current += frameDelay;
double now = System.currentTimeMillis();
double delay = (current - now);
if (delay >= 1.0) {
JF.sleep((int)delay);
}
}
output.stop();
JFLog.log("play thread exit");
}
}
public class PlayAudioOnlyThread extends Thread {
public void run() {
double frameDelay = 1000.0 / ((44100.0 * chs) / (audio_bufsiz));
double samplesPerFrame = audio_bufsiz;
double samplesToWrite = 0;
AudioOutput output = new AudioOutput();
output.start(chs, 44100, 16, audio_bufsiz * 2 /*bytes*/, "<default>");
short samples[] = new short[audio_bufsiz];
double current = System.currentTimeMillis();
while (playing) {
if (preBuffering) {
//wait till buffers are 50% full before starting
while (!eof && playing && preBuffering) {
if (audio_buffer.size() >= (44100 * chs * pre_buffer_seconds)) break;
JF.sleep(25);
}
preBuffering = false;
for(int a=0;a<2;a++) output.write(samples); //prime output
}
samplesToWrite += samplesPerFrame;
if (eof) {
if (audio_buffer.size() < audio_bufsiz) break;
}
while (audio_buffer.size() >= audio_bufsiz && samplesToWrite >= audio_bufsiz) {
audio_buffer.get(samples, 0, audio_bufsiz);
output.write(samples);
samplesToWrite -= audio_bufsiz;
synchronized(countLock) { audioCount += audio_bufsiz; };
}
current += frameDelay;
double now = System.currentTimeMillis();
double delay = (current - now);
if (delay >= 1.0) {
JF.sleep((int)delay);
}
}
output.stop();
JFLog.log("play thread exit");
}
}
public void setVideoSize(int width,int height) {
synchronized(sizeLock) {
new_width = width;
new_height = height;
resizeVideo = true;
}
}
public void setPanel(JPanel panel) {
JFLog.log("setPanel:" + panel);
MediaApp.frame.setContentPane(panel);
panel.revalidate();
}
public void playOrPause() {
if (paused) resume(); else pause();
}
public void seek() {
if (!playing) return;
if (updatingPos) return;
if (videoPanel != null)
seekPosition = videoPanel.time().getValue();
else
seekPosition = time.getValue();
// JFLog.log("seek=" + seekPosition);
}
}