/*
This file is part of jpcsp.
Jpcsp 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.
Jpcsp 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 Jpcsp. If not, see <http://www.gnu.org/licenses/>.
*/
package jpcsp.GUI;
import static jpcsp.HLE.modules.sceAudiocodec.PSP_CODEC_AT3PLUS;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_CIRCLE;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_CROSS;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_DOWN;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_LEFT;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_LTRIGGER;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_RIGHT;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_RTRIGGER;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_TRIANGLE;
import static jpcsp.HLE.modules.sceCtrl.PSP_CTRL_UP;
import static jpcsp.HLE.modules.sceMpeg.UNKNOWN_TIMESTAMP;
import static jpcsp.HLE.modules.sceMpeg.mpegTimestampPerSecond;
import static jpcsp.HLE.modules.sceUtility.PSP_SYSTEMPARAM_BUTTON_CROSS;
import static jpcsp.HLE.modules.sceUtility.getSystemParamButtonPreference;
import static jpcsp.format.psmf.PsmfAudioDemuxVirtualFile.PACK_START_CODE;
import static jpcsp.format.psmf.PsmfAudioDemuxVirtualFile.PADDING_STREAM;
import static jpcsp.format.psmf.PsmfAudioDemuxVirtualFile.PRIVATE_STREAM_1;
import static jpcsp.format.psmf.PsmfAudioDemuxVirtualFile.PRIVATE_STREAM_2;
import static jpcsp.format.psmf.PsmfAudioDemuxVirtualFile.SYSTEM_HEADER_START_CODE;
import static jpcsp.util.Utilities.endianSwap16;
import static jpcsp.util.Utilities.endianSwap32;
import static jpcsp.util.Utilities.sleep;
import java.awt.BorderLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.Image;
import java.awt.image.BufferedImage;
import java.awt.image.MemoryImageSource;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.LinkedList;
import java.util.List;
import javax.imageio.ImageIO;
import javax.sound.sampled.AudioFormat;
import javax.sound.sampled.AudioSystem;
import javax.sound.sampled.DataLine;
import javax.sound.sampled.LineUnavailableException;
import javax.sound.sampled.SourceDataLine;
import javax.swing.ImageIcon;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.OverlayLayout;
import org.apache.log4j.Logger;
import jpcsp.Emulator;
import jpcsp.MainGUI;
import jpcsp.MemoryMap;
import jpcsp.State;
import jpcsp.filesystems.umdiso.UmdIsoFile;
import jpcsp.filesystems.umdiso.UmdIsoReader;
import jpcsp.format.RCO;
import jpcsp.format.psmf.PesHeader;
import jpcsp.format.rco.Display;
import jpcsp.format.rco.RCOState;
import jpcsp.format.rco.vsmx.objects.MoviePlayer;
import jpcsp.hardware.Screen;
import jpcsp.media.codec.CodecFactory;
import jpcsp.media.codec.ICodec;
import jpcsp.media.codec.IVideoCodec;
import jpcsp.media.codec.h264.H264Utils;
import jpcsp.memory.IMemoryReader;
import jpcsp.memory.IMemoryWriter;
import jpcsp.memory.MemoryReader;
import jpcsp.memory.MemoryWriter;
import jpcsp.util.Utilities;
import jpcsp.HLE.Modules;
import jpcsp.HLE.modules.sceMpeg;
import jpcsp.HLE.modules.sceUtility;
import com.twilight.h264.decoder.GetBitContext;
import com.twilight.h264.decoder.H264Context;
public class UmdVideoPlayer implements KeyListener {
private static Logger log = Logger.getLogger("videoplayer");
private static final boolean dumpFrames = false;
// ISO file
private String fileName;
private UmdIsoReader iso;
private UmdIsoFile isoFile;
// Stream storage
private List<MpsStreamInfo> mpsStreams;
private int currentStreamIndex;
// Display
private JLabel display;
private Display rcoDisplay;
private int screenWidth;
private int screenHeigth;
private Image image;
private int resizeScaleFactor;
private MainGUI gui;
// Video
private IVideoCodec videoCodec;
private boolean videoCodecInit;
private int[] videoData = new int[0x10000];
private int videoDataOffset;
private int videoChannel = 0;
public static int frame;
private int videoWidth;
private int videoHeight;
private int videoAspectRatioNum;
private int videoAspectRatioDen;
private int[] luma;
private int[] cr;
private int[] cb;
private int[] abgr;
private boolean foundFrameStart;
private int parseState;
private int[] parseHistory = new int[6];
private int parseHistoryCount;
private int parseLastMb;
private int nalLengthSize = 0;
private boolean isAvc = false;
private int lastParsePosition;
// Audio
private ICodec audioCodec;
private boolean audioCodecInitialized;
private int[] audioData = new int[0x10000];
private int audioDataOffset;
private int audioFrameLength;
private int audioChannels;
private final int frameHeader[] = new int[8];
private int frameHeaderLength;
private int audioChannel = 0;
private int samplesAddr = MemoryMap.START_USERSPACE;
private int audioBufferAddr = MemoryMap.START_USERSPACE + 0x10000;
private byte[] audioBytes;
// Time synchronization
private PesHeader pesHeaderAudio;
private PesHeader pesHeaderVideo;
private long currentVideoTimestamp;
private int currentChapterNumber;
private long startTime;
private int fastForwardSpeed;
private int fastRewindSpeed;
private static final int fastForwardSpeeds[] = new int[] { 1, 50, 100, 200, 400 };
private static final int fastRewindSpeeds[] = new int[] { 1, 50, 100, 200, 400 };
// State (for sync thread).
private volatile boolean videoPaused;
private volatile boolean done;
private volatile boolean endOfVideo;
private volatile boolean threadExit;
// Internal data
private MpsDisplayThread displayThread;
private SourceDataLine mLine;
// RCO MoviePlayer
private MoviePlayer moviePlayer;
private RCOState rcoState;
private DisplayControllerThread displayControllerThread;
// MPS stream class.
protected class MpsStreamInfo {
private String streamName;
private int streamWidth;
private int streamHeigth;
private int streamFirstTimestamp;
private int streamLastTimestamp;
private MpsStreamMarkerInfo[] streamMarkers;
private int playListNumber;
public MpsStreamInfo(String name, int width, int heigth, int firstTimestamp, int lastTimestamp, MpsStreamMarkerInfo[] markers, int playListNumber) {
streamName = name;
streamWidth = width;
streamHeigth = heigth;
streamFirstTimestamp = firstTimestamp;
streamLastTimestamp = lastTimestamp;
streamMarkers = markers;
this.playListNumber = playListNumber;
}
public String getName() {
return streamName;
}
public int getWidth() {
return streamWidth;
}
public int getHeigth() {
return streamHeigth;
}
public int getFirstTimestamp() {
return streamFirstTimestamp;
}
public int getLastTimestamp() {
return streamLastTimestamp;
}
public MpsStreamMarkerInfo[] getMarkers() {
return streamMarkers;
}
public int getPlayListNumber() {
return playListNumber;
}
public int getChapterNumber(long timestamp) {
int marker = -1;
if (streamMarkers != null) {
for (int i = 0; i < streamMarkers.length; i++) {
if (streamMarkers[i].getTimestamp() <= timestamp) {
marker = i;
} else {
break;
}
}
}
return marker;
}
@Override
public String toString() {
StringBuilder s = new StringBuilder();
s.append(String.format("name='%s', %dx%d, %s(%d to %d), markers=[", getName(), getWidth(), getHeigth(), getTimestampString(getLastTimestamp() - getFirstTimestamp()), getFirstTimestamp(), getLastTimestamp()));
for (int i = 0; i < streamMarkers.length; i++) {
if (i > 0) {
s.append(", ");
}
s.append(streamMarkers[i]);
}
s.append("]");
return s.toString();
}
}
// MPS stream's marker class.
protected class MpsStreamMarkerInfo {
private String streamMarkerName;
private long streamMarkerTimestamp;
public MpsStreamMarkerInfo(String name, long timestamp) {
streamMarkerName = name;
streamMarkerTimestamp = timestamp;
}
public String getName() {
return streamMarkerName;
}
public long getTimestamp() {
return streamMarkerTimestamp;
}
@Override
public String toString() {
return String.format("'%s' %s(timeStamp=%d)", getName(), getTimestampString(getTimestamp()), getTimestamp());
}
}
private static String getTimestampString(long timestamp) {
int seconds = (int) (timestamp / mpegTimestampPerSecond);
int hundredth = (int) (timestamp - ((long) seconds) * mpegTimestampPerSecond);
hundredth = 100 * hundredth / mpegTimestampPerSecond;
int minutes = seconds / 60;
seconds -= minutes * 60;
int hours = minutes / 60;
minutes -= hours * 60;
return String.format("%02d:%02d:%02d.%02d", hours, minutes, seconds, hundredth);
}
public UmdVideoPlayer(MainGUI gui, UmdIsoReader iso) {
this.iso = iso;
display = new JLabel();
rcoDisplay = new Display();
JPanel panel = new JPanel();
panel.setLayout(new OverlayLayout(panel));
panel.add(rcoDisplay);
panel.add(display);
gui.remove(Modules.sceDisplayModule.getCanvas());
gui.getContentPane().add(panel, BorderLayout.CENTER);
gui.addKeyListener(this);
setVideoPlayerResizeScaleFactor(gui, 1);
init();
}
public void exit() {
stopDisplayThread();
}
@Override
public void keyPressed(KeyEvent event) {
State.controller.keyPressed(event);
if (moviePlayer != null) {
if ((State.controller.getButtons() & PSP_CTRL_UP) != 0) {
moviePlayer.onUp();
}
if ((State.controller.getButtons() & PSP_CTRL_DOWN) != 0) {
moviePlayer.onDown();
}
if ((State.controller.getButtons() & PSP_CTRL_LEFT) != 0) {
moviePlayer.onLeft();
}
if ((State.controller.getButtons() & PSP_CTRL_RIGHT) != 0) {
moviePlayer.onRight();
}
int pushButton = getSystemParamButtonPreference() == PSP_SYSTEMPARAM_BUTTON_CROSS ? PSP_CTRL_CROSS : PSP_CTRL_CIRCLE;
if ((State.controller.getButtons() & pushButton) != 0) {
moviePlayer.onPush();
}
// TODO Non-standard key mappings...
if ((State.controller.getButtons() & PSP_CTRL_RTRIGGER) != 0) {
fastForward();
}
if ((State.controller.getButtons() & PSP_CTRL_LTRIGGER) != 0) {
rewind();
}
if ((State.controller.getButtons() & PSP_CTRL_TRIANGLE) != 0) {
resumeVideo();
}
} else {
if (event.getKeyCode() == KeyEvent.VK_RIGHT) {
stopDisplayThread();
goToNextMpsStream();
} else if (event.getKeyCode() == KeyEvent.VK_LEFT && currentStreamIndex > 0) {
stopDisplayThread();
goToPreviousMpsStream();
} else if (event.getKeyCode() == KeyEvent.VK_W && !videoPaused) {
pauseVideo();
} else if (event.getKeyCode() == KeyEvent.VK_S) {
resumeVideo();
} else if (event.getKeyCode() == KeyEvent.VK_A) {
rewind();
} else if (event.getKeyCode() == KeyEvent.VK_D) {
fastForward();
} else if (event.getKeyCode() == KeyEvent.VK_UP) {
if (moviePlayer != null) {
moviePlayer.onUp();
}
} else if (event.getKeyCode() == KeyEvent.VK_DOWN) {
if (moviePlayer != null) {
moviePlayer.onDown();
}
}
}
}
@Override
public void keyReleased(KeyEvent event) {
State.controller.keyReleased(event);
}
@Override
public void keyTyped(KeyEvent keyCode) {
}
private void init() {
Emulator.getScheduler().reset();
Emulator.getClock().resume();
displayControllerThread = new DisplayControllerThread();
displayControllerThread.setName("Display Controller Thread");
displayControllerThread.setDaemon(true);
displayControllerThread.start();
done = false;
threadExit = false;
isoFile = null;
mpsStreams = new LinkedList<UmdVideoPlayer.MpsStreamInfo>();
pauseVideo();
currentStreamIndex = 0;
parsePlaylistFile();
parseRCO();
if (videoPaused) {
goToMpsStream(currentStreamIndex);
}
}
public void setVideoPlayerResizeScaleFactor(MainGUI gui, int resizeScaleFactor) {
this.resizeScaleFactor = resizeScaleFactor;
this.gui = gui;
resizeVideoPlayer();
}
private void resizeVideoPlayer() {
if (videoWidth <= 0 || videoHeight <= 0) {
screenWidth = Screen.width * resizeScaleFactor;
screenHeigth = Screen.height * resizeScaleFactor;
} else {
if (log.isDebugEnabled()) {
log.debug(String.format("video size %dx%d resizeScaleFactor=%d", videoWidth, videoHeight, resizeScaleFactor));
}
screenWidth = videoWidth * videoAspectRatioNum / videoAspectRatioDen * resizeScaleFactor;
screenHeigth = videoHeight * resizeScaleFactor;
}
gui.setDisplayMinimumSize(screenWidth, screenHeigth);
}
private int readByteHexTo10(UmdIsoFile file) throws IOException {
int hex = file.readByte() & 0xFF;
return (hex >> 4) * 10 + (hex & 0x0F);
}
private void parsePlaylistFile() {
try {
UmdIsoFile file = iso.getFile("UMD_VIDEO/PLAYLIST.UMD");
int umdvMagic = file.readInt();
int umdvVersion = file.readInt();
if (log.isDebugEnabled()) {
log.debug(String.format("Magic 0x%08X, version 0x%08X", umdvMagic, umdvVersion));
}
int globalDataOffset = endianSwap32(file.readInt());
file.seek(globalDataOffset);
int playListSize = endianSwap32(file.readInt());
int playListTracksNum = endianSwap16(file.readShort());
file.skipBytes(2); // NULL.
if (umdvMagic != 0x56444D55) { // UMDV
log.warn("Accessing invalid PLAYLIST.UMD file!");
} else {
log.info(String.format("Accessing valid PLAYLIST.UMD file: playListSize=%d, playListTracksNum=%d", playListSize, playListTracksNum));
}
for (int i = 0; i < playListTracksNum; i++) {
file.skipBytes(2); // 0x035C.
file.skipBytes(2); // 0x0310.
file.skipBytes(2); // 0x0332.
file.skipBytes(30); // NULL.
file.skipBytes(2); // 0x02E8.
int unknown = endianSwap16(file.readShort());
int releaseDateYear = readByteHexTo10(file) * 100 + readByteHexTo10(file);
int releaseDateDay = file.readByte();
int releaseDateMonth = file.readByte();
file.skipBytes(4); // NULL.
file.skipBytes(4); // Unknown (found 0x00000900).
int nameLength = file.readByte() & 0xFF;
byte[] nameBuffer = new byte[nameLength];
file.read(nameBuffer);
String name = new String(nameBuffer);
file.skipBytes(732 - nameLength); // Unknown NULL area with size 0x2DC.
int streamHeight = (int) (file.readByte() * 0x10); // Stream's original height.
file.skipBytes(2); // NULL.
file.skipBytes(4); // 0x00010000.
file.skipBytes(1); // NULL.
int streamWidth = (int) (file.readByte() * 0x10); // Stream's original width.
file.skipBytes(1); // NULL.
int streamNameCharsNum = (int) file.readByte(); // Stream's name non null characters count.
byte[] stringBuf = new byte[streamNameCharsNum];
file.read(stringBuf);
String streamName = new String(stringBuf);
file.skipBytes(8 - streamNameCharsNum); // NULL chars.
file.skipBytes(2); // NULL.
int streamFirstTimestamp = endianSwap32(file.readInt());
file.skipBytes(2); // NULL.
int streamLastTimestamp = endianSwap32(file.readInt());
file.skipBytes(2); // NULL.
int streamMarkerDataLength = endianSwap16(file.readShort()); // Stream's markers' data length.
MpsStreamMarkerInfo[] streamMarkers;
if (streamMarkerDataLength > 0) {
int streamMarkersNum = endianSwap16(file.readShort()); // Stream's number of markers.
streamMarkers = new MpsStreamMarkerInfo[streamMarkersNum];
for (int j = 0; j < streamMarkersNum; j++) {
file.skipBytes(1); // 0x05.
int streamMarkerCharsNum = (int) file.readByte(); // Marker name length.
file.skipBytes(4); // NULL.
long streamMarkerTimestamp = endianSwap32(file.readInt()) & 0xFFFFFFFFL;
file.skipBytes(2); // NULL.
file.skipBytes(4); // NULL.
byte[] markerBuf = new byte[streamMarkerCharsNum];
file.read(markerBuf);
String markerName = new String(markerBuf);
file.skipBytes(24 - streamMarkerCharsNum);
streamMarkers[j] = new MpsStreamMarkerInfo(markerName, streamMarkerTimestamp);
}
file.skip(2); // NULL
} else {
streamMarkers = new MpsStreamMarkerInfo[0];
}
// Map this stream.
MpsStreamInfo info = new MpsStreamInfo(streamName, streamWidth, streamHeight, streamFirstTimestamp, streamLastTimestamp, streamMarkers, i + 1);
if (log.isDebugEnabled()) {
log.debug(String.format("Release date %d-%d-%d, name '%s', unknown=0x%04X", releaseDateYear, releaseDateMonth, releaseDateDay, name, unknown));
log.debug(String.format("StreamInfo #%d: %s", i, info));
}
mpsStreams.add(info);
}
} catch (Exception e) {
log.error("parsePlaylistFile", e);
}
}
private void parseRCO() {
try {
String[] resources = iso.listDirectory("UMD_VIDEO/RESOURCE");
if (resources == null || resources.length <= 0) {
return;
}
int preferredLanguage = sceUtility.getSystemParamLanguage();
String languagePrefix = "EN";
switch (preferredLanguage) {
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_JAPANESE: languagePrefix = "JA"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_ENGLISH: languagePrefix = "EN"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_FRENCH: languagePrefix = "FR"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_SPANISH: languagePrefix = "ES"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_GERMAN: languagePrefix = "DE"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_ITALIAN: languagePrefix = "IT"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_DUTCH: languagePrefix = "NL"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_PORTUGUESE: languagePrefix = "PO"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_RUSSIAN: languagePrefix = "RU"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_KOREAN: languagePrefix = "KO"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_CHINESE_TRADITIONAL: languagePrefix = "CN"; break;
case sceUtility.PSP_SYSTEMPARAM_LANGUAGE_CHINESE_SIMPLIFIED: languagePrefix = "CN"; break;
}
// The resource names are tried in this order:
final String resourceNames[] = new String[] {
"100000",
"000000",
"110000",
"010000"
};
String resourceFileName = null;
for (String resourceName : resourceNames) {
String fileName = languagePrefix + resourceName + ".RCO";
if (iso.hasFile("UMD_VIDEO/RESOURCE/" + fileName)) {
resourceFileName = fileName;
break;
}
}
if (resourceFileName != null) {
if (log.isDebugEnabled()) {
log.debug(String.format("Reading RCO file '%s'", resourceFileName));
}
UmdIsoFile file = iso.getFile("UMD_VIDEO/RESOURCE/" + resourceFileName);
byte[] buffer = new byte[(int) file.length()];
file.read(buffer);
RCO rco = new RCO(buffer);
if (log.isDebugEnabled()) {
log.debug(String.format("RCO: %s", rco));
}
rcoState = rco.execute(this, resourceFileName.replace(".RCO", ""));
}
} catch (FileNotFoundException e) {
} catch (IOException e) {
log.error("parse RCO", e);
}
}
public void changeResource(String resourceName) {
try {
UmdIsoFile file = iso.getFile(String.format("UMD_VIDEO/RESOURCE/%s.RCO", resourceName));
if (log.isDebugEnabled()) {
log.debug(String.format("Reading RCO file '%s.RCO'", resourceName));
}
byte[] buffer = new byte[(int) file.length()];
file.read(buffer);
RCO rco = new RCO(buffer);
if (log.isDebugEnabled()) {
log.debug(String.format("RCO: %s", rco));
}
getRCODisplay().changeResource();
rcoState = rco.execute(rcoState, this, resourceName);
} catch (FileNotFoundException e) {
} catch (IOException e) {
log.error("changeResource", e);
}
}
private int getStreamIndexFromPlayListNumber(int playListNumber) {
for (int i = 0; i < mpsStreams.size(); i++) {
MpsStreamInfo info = mpsStreams.get(i);
if (info.getPlayListNumber() == playListNumber) {
return i;
}
}
return playListNumber;
}
public void setMoviePlayer(MoviePlayer moviePlayer) {
this.moviePlayer = moviePlayer;
}
public void play(int playListNumber, int chapterNumber, int videoNumber, int audioNumber, int audioFlag, int subtitleNumber, int subtitleFlag) {
done = false;
int streamIndex = getStreamIndexFromPlayListNumber(playListNumber);
goToMpsStream(streamIndex);
}
private boolean goToMpsStream(int streamIndex) {
if (streamIndex < 0 || streamIndex >= mpsStreams.size()) {
return false;
}
currentStreamIndex = streamIndex;
MpsStreamInfo info = mpsStreams.get(currentStreamIndex);
fileName = "UMD_VIDEO/STREAM/" + info.getName() + ".MPS";
log.info("Loading stream: " + fileName);
try {
isoFile = iso.getFile(fileName);
String cpiFileName = "UMD_VIDEO/CLIPINF/" + info.getName() + ".CLP";
UmdIsoFile cpiFile = iso.getFile(cpiFileName);
if (cpiFile != null) {
log.info("Found CLIPINF data for this stream: " + cpiFileName);
}
} catch (FileNotFoundException e) {
} catch (IOException e) {
Emulator.log.error(e);
}
if (isoFile != null) {
startVideo();
initVideo();
}
return true;
}
private boolean goToNextMpsStream() {
return goToMpsStream(currentStreamIndex + 1);
}
private boolean goToPreviousMpsStream() {
return goToMpsStream(currentStreamIndex - 1);
}
public void initVideo() {
done = false;
videoPaused = false;
if (displayThread == null) {
displayThread = new MpsDisplayThread();
displayThread.setDaemon(true);
displayThread.setName("UMD Video Player Thread");
displayThread.start();
}
}
public void pauseVideo() {
videoPaused = true;
}
public void resumeVideo() {
if (log.isDebugEnabled()) {
log.debug(String.format("Resume video"));
}
videoPaused = false;
fastForwardSpeed = 0;
fastRewindSpeed = 0;
}
public void fastForward() {
if (fastRewindSpeed > 0) {
fastRewindSpeed--;
} else {
fastForwardSpeed = Math.min(fastForwardSpeeds.length - 1, fastForwardSpeed + 1);
}
if (log.isDebugEnabled()) {
log.debug(String.format("Fast forward %d, fast rewind %d", fastForwardSpeed, fastRewindSpeed));
}
}
public void rewind() {
if (fastForwardSpeed > 0) {
fastForwardSpeed--;
} else {
fastRewindSpeed = Math.min(fastRewindSpeeds.length - 1, fastRewindSpeed + 1);
}
if (log.isDebugEnabled()) {
log.debug(String.format("Fast forward %d, fast rewind %d", fastForwardSpeed, fastRewindSpeed));
}
}
private int read8() {
try {
return isoFile.read();
} catch (IOException e) {
// Ignore exception
}
return -1;
}
private int read16() {
return (read8() << 8) | read8();
}
private int read32() {
return (read8() << 24) | (read8() << 16) | (read8() << 8) | read8();
}
private void skip(int n) {
if (n > 0) {
try {
isoFile.skip(n);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private long readPts(int c) {
return (((long) (c & 0x0E)) << 29) | ((read16() >> 1) << 15) | (read16() >> 1);
}
private long readPts() {
return readPts(read8());
}
private int readPesHeader(int startCode, PesHeader pesHeader) {
int pesLength = 0;
int c = read8();
pesLength++;
while (c == 0xFF) {
c = read8();
pesLength++;
}
if ((c & 0xC0) == 0x40) {
skip(1);
c = read8();
pesLength += 2;
}
pesHeader.setDtsPts(UNKNOWN_TIMESTAMP);
if ((c & 0xE0) == 0x20) {
pesHeader.setDtsPts(readPts(c));
pesLength += 4;
if ((c & 0x10) != 0) {
pesHeader.setPts(readPts());
pesLength += 5;
}
} else if ((c & 0xC0) == 0x80) {
int flags = read8();
int headerLength = read8();
pesLength += 2;
pesLength += headerLength;
if ((flags & 0x80) != 0) {
pesHeader.setDtsPts(readPts());
headerLength -= 5;
if ((flags & 0x40) != 0) {
pesHeader.setDts(readPts());
headerLength -= 5;
}
}
if ((flags & 0x3F) != 0 && headerLength == 0) {
flags &= 0xC0;
}
if ((flags & 0x01) != 0) {
int pesExt = read8();
headerLength--;
int skip = (pesExt >> 4) & 0x0B;
skip += skip & 0x09;
if ((pesExt & 0x40) != 0 || skip > headerLength) {
pesExt = skip = 0;
}
skip(skip);
headerLength -= skip;
if ((pesExt & 0x01) != 0) {
int ext2Length = read8();
headerLength--;
if ((ext2Length & 0x7F) != 0) {
int idExt = read8();
headerLength--;
if ((idExt & 0x80) == 0) {
startCode = ((startCode & 0xFF) << 8) | idExt;
}
}
}
}
skip(headerLength);
}
if (startCode == 0x1BD) { // PRIVATE_STREAM_1
int channel = read8();
pesHeader.setChannel(channel);
pesLength++;
if (channel >= 0x80 && channel <= 0xCF) {
skip(3);
pesLength += 3;
if (channel >= 0xB0 && channel <= 0xBF) {
skip(1);
pesLength++;
}
} else {
skip(3);
pesLength += 3;
}
}
return pesLength;
}
private byte[] resize(byte[] array, int size) {
if (array == null) {
return new byte[size];
}
if (size <= array.length) {
return array;
}
byte[] newArray = new byte[size];
System.arraycopy(array, 0, newArray, 0, array.length);
return newArray;
}
private int[] resize(int[] array, int size) {
if (array == null) {
return new int[size];
}
if (size <= array.length) {
return array;
}
int[] newArray = new int[size];
System.arraycopy(array, 0, newArray, 0, array.length);
return newArray;
}
private void addVideoData(int length, long position) {
videoData = resize(videoData, videoDataOffset + length);
for (int i = 0; i < length; i++) {
videoData[videoDataOffset++] = read8();
}
}
private void addAudioData(int length) {
audioData = resize(audioData, audioDataOffset + length);
while (length > 0) {
int currentFrameLength = audioFrameLength == 0 ? 0 : audioDataOffset % audioFrameLength;
if (currentFrameLength == 0) {
// 8 bytes header:
// - byte 0: 0x0F
// - byte 1: 0xD0
// - byte 2: 0x28
// - byte 3: (frameLength - 8) / 8
// - bytes 4-7: 0x00
while (frameHeaderLength < frameHeader.length && length > 0) {
frameHeader[frameHeaderLength++] = read8();
length--;
}
if (frameHeaderLength < frameHeader.length) {
// Frame header not yet complete
break;
}
if (length == 0) {
// Frame header is complete but no data is following the header.
// Retry when some data is available
break;
}
int frameHeader23 = (frameHeader[2] << 8) | frameHeader[3];
audioFrameLength = ((frameHeader23 & 0x3FF) << 3) + 8;
if (frameHeader[0] != 0x0F || frameHeader[1] != 0xD0) {
if (log.isInfoEnabled()) {
log.warn(String.format("Audio frame length 0x%X with incorrect header (header: %02X %02X %02X %02X %02X %02X %02X %02X)", audioFrameLength, frameHeader[0], frameHeader[1], frameHeader[2], frameHeader[3], frameHeader[4], frameHeader[5], frameHeader[6], frameHeader[7]));
}
} else if (log.isTraceEnabled()) {
log.trace(String.format("Audio frame length 0x%X (header: %02X %02X %02X %02X %02X %02X %02X %02X)", audioFrameLength, frameHeader[0], frameHeader[1], frameHeader[2], frameHeader[3], frameHeader[4], frameHeader[5], frameHeader[6], frameHeader[7]));
}
frameHeaderLength = 0;
}
int lengthToNextFrame = audioFrameLength - currentFrameLength;
int readLength = Utilities.min(length, lengthToNextFrame);
for (int i = 0; i < readLength; i++) {
audioData[audioDataOffset++] = read8();
}
length -= readLength;
}
}
private long getCurrentFilePosition() {
try {
return isoFile.getFilePointer();
} catch (IOException e) {
}
return -1L;
}
private boolean readPsmfPacket(int videoChannel, int audioChannel) {
while (!done) {
int startCode = read32();
if (startCode == -1) {
// End of file
break;
}
int codeLength, pesLength;
switch (startCode) {
case PACK_START_CODE:
skip(10);
break;
case SYSTEM_HEADER_START_CODE:
skip(14);
break;
case PADDING_STREAM:
case PRIVATE_STREAM_2:
codeLength = read16();
skip(codeLength);
break;
case PRIVATE_STREAM_1: // Audio stream
codeLength = read16();
pesLength = readPesHeader(startCode, pesHeaderAudio);
codeLength -= pesLength;
if (pesHeaderAudio.getChannel() == audioChannel || audioChannel < 0) {
addAudioData(codeLength);
return true;
}
skip(codeLength);
break;
case 0x1E0: case 0x1E1: case 0x1E2: case 0x1E3: // Video streams
case 0x1E4: case 0x1E5: case 0x1E6: case 0x1E7:
case 0x1E8: case 0x1E9: case 0x1EA: case 0x1EB:
case 0x1EC: case 0x1ED: case 0x1EE: case 0x1EF:
codeLength = read16();
if (videoChannel < 0 || startCode - 0x1E0 == videoChannel) {
pesLength = readPesHeader(startCode, pesHeaderVideo);
codeLength -= pesLength;
addVideoData(codeLength, getCurrentFilePosition());
return true;
}
skip(codeLength);
break;
}
}
return false;
}
private void consumeVideoData(int length) {
if (length >= videoDataOffset) {
videoDataOffset = 0;
lastParsePosition = 0;
} else {
System.arraycopy(videoData, length, videoData, 0, videoDataOffset - length);
videoDataOffset -= length;
lastParsePosition -= length;
}
}
private void consumeAudioData(int length) {
if (length >= audioDataOffset) {
audioDataOffset = 0;
} else {
System.arraycopy(audioData, length, audioData, 0, audioDataOffset - length);
audioDataOffset -= length;
}
}
private int startCodeFindCandidate(int offset, int size) {
for (int i = 0; i < size; i++) {
if (videoData[offset + i] == 0x00) {
return i;
}
}
return size;
}
private int findVideoFrameEnd() {
if (parseState > 13) {
parseState = 7;
}
if (lastParsePosition < 0) {
lastParsePosition = 0;
}
int nextAvc = isAvc ? 0 : videoDataOffset;
int found = -1;
for (int i = lastParsePosition; i < videoDataOffset; i++) {
if (i >= nextAvc) {
int nalSize = 0;
i = nextAvc;
for (int j = 0; j < nalLengthSize; j++) {
nalSize = (nalSize << 8) | videoData[i++];
}
if (nalSize <= 0 || nalSize > videoDataOffset - 1) {
return videoDataOffset;
}
nextAvc = i + nalLengthSize;
parseState = 5;
}
if (parseState == 7) {
i += startCodeFindCandidate(i, nextAvc - i);
if (i < nextAvc) {
parseState = 2;
}
} else if (parseState <= 2) {
if (videoData[i] == 1) {
parseState ^= 5; // 2->7, 1->4, 0->5
} else if (videoData[i] != 0) {
parseState = 7;
} else {
parseState >>= 1; // 2->1, 1->0, 0->0
}
} else if (parseState <= 5) {
int naluType = videoData[i] & 0x1F;
if (naluType == H264Context.NAL_SEI || naluType == H264Context.NAL_SPS ||
naluType == H264Context.NAL_PPS || naluType == H264Context.NAL_AUD) {
if (foundFrameStart) {
found = i + 1;
break;
}
} else if (naluType == H264Context.NAL_SLICE || naluType == H264Context.NAL_DPA ||
naluType == H264Context.NAL_IDR_SLICE) {
parseState += 8;
continue;
}
parseState = 7;
} else {
parseHistory[parseHistoryCount++] = videoData[i];
if (parseHistoryCount > 5) {
int lastMb = parseLastMb;
GetBitContext gb = new GetBitContext();
gb.init_get_bits(parseHistory, 0, 8 * parseHistoryCount);
parseHistoryCount = 0;
int mb = gb.get_ue_golomb("UmdVideoPlayer.findVideoFrameEnd");
parseLastMb = mb;
if (foundFrameStart) {
if (mb <= lastMb) {
found = i;
break;
}
} else {
foundFrameStart = true;
}
parseState = 7;
}
}
}
if (found >= 0) {
foundFrameStart = false;
found -= (parseState & 5);
if (parseState > 7) {
found -= 5;
}
parseState = 7;
lastParsePosition = found;
} else {
lastParsePosition = videoDataOffset;
}
return found;
}
public boolean startVideo() {
endOfVideo = false;
videoPaused = false;
videoCodec = CodecFactory.getVideoCodec();
videoCodecInit = false;
videoDataOffset = 0;
videoWidth = 0;
videoHeight = 0;
audioCodec = CodecFactory.getCodec(PSP_CODEC_AT3PLUS);
audioCodecInitialized = false;
audioChannels = 2;
audioDataOffset = 0;
audioFrameLength = 0;
frameHeaderLength = 0;
foundFrameStart = false;
pesHeaderAudio = new PesHeader(audioChannel);
pesHeaderVideo = new PesHeader(videoChannel);
startTime = System.currentTimeMillis();
frame = 0;
currentChapterNumber = -1;
return true;
}
private void stopDisplayThread() {
done = true;
while (displayThread != null && !threadExit) {
sleep(1, 0);
}
}
private void writeFile(int[] values, int size, String name) {
try {
OutputStream os = new FileOutputStream(name);
byte[] bytes = new byte[size];
for (int i = 0; i < size; i++) {
bytes[i] = (byte) values[i];
}
os.write(bytes);
os.close();
} catch (FileNotFoundException e) {
} catch (IOException e) {
}
}
public void stepVideo() {
image = null;
int frameSize = -1;
do {
if (!readPsmfPacket(videoChannel, audioChannel)) {
if (videoDataOffset <= 0) {
// Enf of file reached
break;
}
frameSize = findVideoFrameEnd();
if (frameSize < 0) {
// Process pending last frame
frameSize = videoDataOffset;
}
} else {
frameSize = findVideoFrameEnd();
}
} while (frameSize <= 0 && !done);
if (frameSize <= 0) {
endOfVideo = true;
return;
}
if (!videoCodecInit) {
int[] extraData = null;
int extraDataLength = H264Utils.findExtradata(videoData, 0, frameSize);
if (extraDataLength > 0) {
extraData = new int[extraDataLength];
System.arraycopy(videoData, 0, extraData, 0, extraDataLength);
}
if (videoCodec.init(extraData) == 0) {
videoCodecInit = true;
} else {
endOfVideo = true;
return;
}
}
int consumedLength = videoCodec.decode(videoData, 0, frameSize);
if (consumedLength < 0) {
endOfVideo = true;
return;
}
if (videoCodec.hasImage()) {
int[] aspectRatio = new int[2];
videoCodec.getAspectRatio(aspectRatio);
videoAspectRatioNum = aspectRatio[0];
videoAspectRatioDen = aspectRatio[1];
frame++;
}
consumeVideoData(consumedLength);
boolean skipFrame = false;
if ((frame % fastForwardSpeeds[fastForwardSpeed]) != 0) {
skipFrame = true;
startTime -= sceMpeg.videoTimestampStep;
}
if (videoCodec.hasImage() && !skipFrame) {
int width = videoCodec.getImageWidth();
int height = videoCodec.getImageHeight();
boolean resized = false;
if (videoWidth <= 0) {
videoWidth = width;
resized = true;
}
if (videoHeight <= 0) {
videoHeight = height;
resized = true;
}
if (log.isTraceEnabled()) {
log.trace(String.format("Decoded video frame %dx%d (video %dx%d), pes=%s, SAR %d:%d", width, height, videoWidth, videoHeight, pesHeaderVideo, videoAspectRatioNum, videoAspectRatioDen));
}
if (resized) {
resizeVideoPlayer();
}
int size = width * height;
int size2 = size >> 2;
luma = resize(luma, size);
cr = resize(cr, size2);
cb = resize(cb, size2);
if (videoCodec.getImage(luma, cb, cr) == 0) {
if (dumpFrames) {
writeFile(luma, size, String.format("Frame%d.y", frame));
writeFile(cb, size2, String.format("Frame%d.cb", frame));
writeFile(cr, size2, String.format("Frame%d.cr", frame));
}
abgr = resize(abgr, size);
// TODO How to find out if we have a YUVJ image?
// H264Utils.YUVJ2YUV(luma, luma, size);
H264Utils.YUV2ARGB(width, height, luma, cb, cr, abgr);
image = display.createImage(new MemoryImageSource(videoWidth, videoHeight, abgr, 0, width));
long now = System.currentTimeMillis();
long currentDuration = now - startTime;
long videoDuration = frame * 100000L / sceMpeg.videoTimestampStep;
if (currentDuration < videoDuration) {
Utilities.sleep((int) (videoDuration - currentDuration), 0);
}
}
}
if (videoCodec.hasImage()) {
if (pesHeaderVideo.getPts() != UNKNOWN_TIMESTAMP) {
currentVideoTimestamp = pesHeaderVideo.getPts();
} else {
currentVideoTimestamp += sceMpeg.videoTimestampStep;
}
if (log.isTraceEnabled()) {
MpsStreamInfo streamInfo = mpsStreams.get(currentStreamIndex);
log.trace(String.format("Playing stream %d: %s / %s", currentStreamIndex, getTimestampString(currentVideoTimestamp - streamInfo.streamFirstTimestamp), getTimestampString(streamInfo.streamLastTimestamp - streamInfo.streamFirstTimestamp)));
}
}
if (pesHeaderVideo.getPts() != UNKNOWN_TIMESTAMP) {
int chapterNumber = mpsStreams.get(currentStreamIndex).getChapterNumber(pesHeaderVideo.getPts());
if (chapterNumber != currentChapterNumber) {
if (moviePlayer != null) {
// For the MoviePlayer, chapters are numbered starting from 1
moviePlayer.onChapter(chapterNumber + 1);
}
currentChapterNumber = chapterNumber;
}
}
if (audioFrameLength > 0 && audioDataOffset >= audioFrameLength) {
if (!audioCodecInitialized) {
audioCodec.init(audioFrameLength, audioChannels, audioChannels, 0);
AudioFormat audioFormat = new AudioFormat(44100, 16, audioChannels, true, false);
DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat);
try {
mLine = (SourceDataLine) AudioSystem.getLine(info);
mLine.open(audioFormat);
} catch (LineUnavailableException e) {
// Ignore error
}
mLine.start();
audioCodecInitialized = true;
}
int result = -1;
if (fastForwardSpeed == 0) {
IMemoryWriter memoryWriter = MemoryWriter.getMemoryWriter(audioBufferAddr, audioFrameLength, 1);
for (int i = 0; i < audioFrameLength; i++) {
memoryWriter.writeNext(audioData[i]);
}
memoryWriter.flush();
result = audioCodec.decode(audioBufferAddr, audioFrameLength, samplesAddr);
}
consumeAudioData(audioFrameLength);
if (result > 0) {
int audioBytesLength = audioCodec.getNumberOfSamples() * 2 * audioChannels;
audioBytes = resize(audioBytes, audioBytesLength);
IMemoryReader memoryReader = MemoryReader.getMemoryReader(samplesAddr, audioBytesLength, 1);
for (int i = 0; i < audioBytesLength; i++) {
audioBytes[i] = (byte) memoryReader.readNext();
}
mLine.write(audioBytes, 0, audioBytesLength);
}
}
}
public void takeScreenshot() {
int tag = 0;
String screenshotName = State.title + "-" + "Shot" + "-" + tag + ".png";
File screenshot = new File(screenshotName);
File directory = new File(System.getProperty("user.dir"));
for(File file : directory.listFiles()) {
if (file.getName().contains(State.title + "-" + "Shot")) {
screenshotName = State.title + "-" + "Shot" + "-" + ++tag + ".png";
screenshot = new File(screenshotName);
}
}
try {
BufferedImage img = (BufferedImage)getImage();
ImageIO.write(img, "png", screenshot);
img.flush();
} catch (Exception e) {
return;
}
}
private Image getImage() {
return image;
}
public Display getRCODisplay() {
return rcoDisplay;
}
private class DisplayControllerThread extends Thread {
private volatile boolean done = false;
@Override
public void run() {
while (!done) {
Emulator.getScheduler().step();
jpcsp.State.controller.hleControllerPoll();
Utilities.sleep(10, 0);
}
}
}
private class MpsDisplayThread extends Thread {
@Override
public void run() {
if (log.isTraceEnabled()) {
log.trace(String.format("Starting Mps Display thread"));
}
threadExit = false;
while (!done) {
while (!endOfVideo && !done) {
if (!videoPaused) {
stepVideo();
if (display != null && image != null) {
Image scaledImage = getImage();
if (videoWidth != screenWidth || videoHeight != screenHeigth) {
if (log.isTraceEnabled()) {
log.trace(String.format("Scaling video image from %dx%d to %dx%d", videoWidth, videoHeight, screenWidth, screenHeigth));
}
scaledImage = scaledImage.getScaledInstance(screenWidth, screenHeigth, Image.SCALE_SMOOTH);
}
display.setIcon(new ImageIcon(scaledImage));
}
} else {
Utilities.sleep(10, 0);
}
}
if (!done) {
if (moviePlayer != null) {
done = true;
moviePlayer.onPlayListEnd(mpsStreams.get(currentStreamIndex).getPlayListNumber());
} else {
if (log.isTraceEnabled()) {
log.trace(String.format("Switching to next stream"));
}
if (!goToNextMpsStream()) {
done = true;
}
}
}
}
threadExit = true;
displayThread = null;
if (log.isTraceEnabled()) {
log.trace(String.format("Exiting Mps Display thread"));
}
}
}
}