/* 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.test; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.awt.image.MemoryImageSource; import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; import java.util.Arrays; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.DataLine; import javax.sound.sampled.SourceDataLine; import javax.swing.JFrame; import org.apache.log4j.Logger; import org.apache.log4j.xml.DOMConfigurator; import jpcsp.Memory; import jpcsp.MemoryMap; import jpcsp.HLE.modules.sceAudiocodec; import jpcsp.HLE.modules.sceMpeg; import jpcsp.media.codec.CodecFactory; import jpcsp.media.codec.ICodec; import jpcsp.util.Utilities; import com.twilight.h264.decoder.AVFrame; import com.twilight.h264.decoder.AVPacket; import com.twilight.h264.decoder.H264Context; import com.twilight.h264.decoder.H264Decoder; import com.twilight.h264.decoder.MpegEncContext; import com.twilight.h264.player.PlayerFrame; import com.twilight.h264.util.FrameUtils; public class PSMFPlayer implements Runnable { private static Logger log = Logger.getLogger("PSMFPlayer"); private static final int progressHeight = 2; private PlayerFrame displayPanel; private String fileName; private int[] buffer = null; private int[] videoData = new int[0x10000]; private int[] audioData = new int[0x10000]; private int videoDataOffset; private int audioDataOffset; private int audioFrameLength; private final int frameHeader[] = new int[8]; private int frameHeaderLength; private InputStream is; private int totalNumberOfFrames; public static void main(String[] args) { new PSMFPlayer(args); } public PSMFPlayer(String[] args) { if(args.length<1) { System.out.println("Usage: java jpcsp.PSMFPlayer <.pmf file>\n"); return; } JFrame frame = new JFrame("Player"); displayPanel = new PlayerFrame(); frame.getContentPane().add(displayPanel, BorderLayout.CENTER); // Finish setting up the frame, and show it. frame.addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { System.exit(0); } }); displayPanel.setVisible(true); // Standard PSP screen dimensions displayPanel.setPreferredSize(new Dimension(480, 272 + progressHeight)); frame.pack(); frame.setVisible(true); fileName = args[0]; new Thread(this).start(); } @Override public void run() { System.out.println("Playing "+ fileName); playFile(fileName); } private int read8(InputStream is) { try { return is.read(); } catch (IOException e) { e.printStackTrace(); } return -1; } private int read16(InputStream is) { return (read8(is) << 8) | read8(is); } private int read32(InputStream is) { return (read8(is) << 24) | (read8(is) << 16) | (read8(is) << 8) | read8(is); } private long readTimestamp(InputStream is) { long timestamp = (read16(is) & 0xFFFFL) << 32; timestamp |= read32(is) & 0xFFFFFFFFL; return timestamp; } private void skip(InputStream is, int n) { if (n > 0) { try { is.skip(n); } catch (IOException e) { e.printStackTrace(); } } } private int skipPesHeader(InputStream is, int startCode) { int pesLength = 0; int c = read8(is); pesLength++; while (c == 0xFF) { c = read8(is); pesLength++; } if ((c & 0xC0) == 0x40) { skip(is, 1); c = read8(is); pesLength += 2; } if ((c & 0xE0) == 0x20) { skip(is, 4); pesLength += 4; if ((c & 0x10) != 0) { skip(is, 5); pesLength += 5; } } else if ((c & 0xC0) == 0x80) { skip(is, 1); int headerLength = read8(is); pesLength += 2; skip(is, headerLength); pesLength += headerLength; } if (startCode == 0x1BD) { // PRIVATE_STREAM_1 int channel = read8(is); pesLength++; if (channel >= 0x80 && channel <= 0xCF) { skip(is, 3); pesLength += 3; if (channel >= 0xB0 && channel <= 0xBF) { skip(is, 1); pesLength++; } } else { skip(is, 3); pesLength += 3; } } return pesLength; } private void addVideoData(InputStream is, int length) { if (videoDataOffset + length > videoData.length) { // Extend the inputBuffer int[] newInputBuffer = new int[videoDataOffset + length]; System.arraycopy(videoData, 0, newInputBuffer, 0, videoDataOffset); videoData = newInputBuffer; } for (int i = 0; i < length; i++) { videoData[videoDataOffset++] = read8(is); } } private void addAudioData(InputStream is, int length) { if (audioDataOffset + length > audioData.length) { // Extend the inputBuffer int[] newInputBuffer = new int[audioDataOffset + length]; System.arraycopy(audioData, 0, newInputBuffer, 0, audioDataOffset); audioData = newInputBuffer; } 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(is); 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) { 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])); } frameHeaderLength = 0; } int lengthToNextFrame = audioFrameLength - currentFrameLength; int readLength = Utilities.min(length, lengthToNextFrame); for (int i = 0; i < readLength; i++) { audioData[audioDataOffset++] = read8(is); } length -= readLength; } } private void readPsmfHeader(File f) { try { is = new FileInputStream(f); } catch (FileNotFoundException e) { e.printStackTrace(); return; } if (read32(is) != 0x50534D46) { // PSMF return; } skip(is, 4); int mpegOffset = read32(is); skip(is, sceMpeg.PSMF_FIRST_TIMESTAMP_OFFSET - 12); long firstTimestamp = readTimestamp(is); long lastTimestamp = readTimestamp(is); totalNumberOfFrames = (int) ((lastTimestamp - firstTimestamp) / sceMpeg.videoTimestampStep); skip(is, mpegOffset - (sceMpeg.PSMF_LAST_TIMESTAMP_OFFSET + 6)); } private boolean readPsmfPacket(int videoChannel, int audioChannel) { while (true) { int startCode = read32(is); if (startCode == -1) { // End of file return false; } int codeLength; switch (startCode) { case 0x1BA: // PACK_START_CODE skip(is, 10); break; case 0x1BB: // SYSTEM_HEADER_START_CODE skip(is, 14); break; case 0x1BE: // PADDING_STREAM case 0x1BF: // PRIVATE_STREAM_2 codeLength = read16(is); skip(is, codeLength); break; case 0x1BD: { // PRIVATE_STREAM_1 // Audio stream codeLength = read16(is); int pesLength = skipPesHeader(is, startCode); codeLength -= pesLength; addAudioData(is, 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(is); if (videoChannel < 0 || startCode - 0x1E0 == videoChannel) { int pesLength = skipPesHeader(is, startCode); codeLength -= pesLength; addVideoData(is, codeLength); return true; } skip(is, codeLength); break; } } } private void consumeVideoData(int length) { if (length >= videoDataOffset) { videoDataOffset = 0; } else { System.arraycopy(videoData, length, videoData, 0, videoDataOffset - length); videoDataOffset -= length; } } private void consumeAudioData(int length) { if (length >= audioDataOffset) { audioDataOffset = 0; } else { System.arraycopy(audioData, length, audioData, 0, audioDataOffset - length); audioDataOffset -= length; } } private int findFrameEnd() { for (int i = 5; i < videoDataOffset; i++) { if (videoData[i - 4] == 0x00 && videoData[i - 3] == 0x00 && videoData[i - 2] == 0x00 && videoData[i - 1] == 0x01) { int naluType = videoData[i] & 0x1F; if (naluType == H264Context.NAL_AUD) { return i - 4; } } } return -1; } public boolean playFile(String filename) { DOMConfigurator.configure("LogSettings.xml"); H264Decoder codec; MpegEncContext c= null; int[] got_picture = new int[1]; File f = new File(filename); AVFrame picture; AVPacket avpkt = new AVPacket(); avpkt.av_init_packet(); System.out.println("Video decoding\n"); /* find the mpeg1 video decoder */ codec = new H264Decoder(); if (codec == null) { System.out.println("codec not found\n"); System.exit(1); } c = MpegEncContext.avcodec_alloc_context(); picture = AVFrame.avcodec_alloc_frame(); if ((codec.capabilities & H264Decoder.CODEC_CAP_TRUNCATED) != 0) { c.flags |= MpegEncContext.CODEC_FLAG_TRUNCATED; /* we do not send complete frames */ } /* For some codecs, such as msmpeg4 and mpeg4, width and height MUST be initialized there because this information is not available in the bitstream. */ /* open it */ if (c.avcodec_open(codec) < 0) { System.out.println("could not open codec\n"); System.exit(1); } boolean showProgress = true; int frame = 0; try { readPsmfHeader(f); if (totalNumberOfFrames <= 0) { showProgress = false; } int videoChannel = 0; int audioChannel = 0; int audioChannels = 2; Memory mem = Memory.getInstance(); int audioInputAddr = MemoryMap.START_USERSPACE + 0x10000; int audioOutputAddr = MemoryMap.START_USERSPACE; ICodec audioCodec = CodecFactory.getCodec(sceAudiocodec.PSP_CODEC_AT3PLUS); boolean audioCodecInit = false; byte[] audioOutputData = null; AudioFormat audioFormat = new AudioFormat(44100, 16, audioChannels, true, false); DataLine.Info info = new DataLine.Info(SourceDataLine.class, audioFormat); SourceDataLine mLine = (SourceDataLine) AudioSystem.getLine(info); mLine.open(audioFormat); log.info(String.format("Audio line buffer size %d", mLine.getBufferSize())); mLine.start(); long startTime = System.currentTimeMillis(); while (true) { int frameSize = -1; do { if (!readPsmfPacket(videoChannel, audioChannel)) { if (videoDataOffset <= 0) { // Enf of file reached break; } frameSize = findFrameEnd(); if (frameSize < 0) { // Process pending last frame frameSize = videoDataOffset; } } else { frameSize = findFrameEnd(); } } while (frameSize <= 0); if (frameSize <= 0) { break; } avpkt.data_base = videoData; avpkt.size = frameSize; avpkt.data_offset = 0; while (avpkt.size > 0) { int len = c.avcodec_decode_video2(picture, got_picture, avpkt); if (len < 0) { log.error("Error while decoding frame "+ frame); // Discard current packet and proceed to next packet break; } // if if (got_picture[0] != 0) { picture = c.priv_data.displayPicture; int imageWidth = picture.imageWidthWOEdge; int imageHeight = picture.imageHeightWOEdge; int bufferSize = imageWidth * imageHeight; if (buffer == null || bufferSize != buffer.length) { buffer = new int[bufferSize]; } FrameUtils.YUV2RGB_WOEdge(picture, buffer); int progress = showProgress ? imageWidth * frame / totalNumberOfFrames : 0; int y = Math.min(272, imageHeight - progressHeight); int offset = y * imageWidth; for (int i = 0; i < progressHeight; i++, offset += imageWidth) { Arrays.fill(buffer, offset, offset + progress, 0xFFFFFFFF); Arrays.fill(buffer, offset + progress, offset + imageWidth, 0xFF000000); } displayPanel.lastFrame = displayPanel.createImage(new MemoryImageSource(imageWidth , imageHeight, buffer, 0, imageWidth)); displayPanel.invalidate(); displayPanel.updateUI(); frame++; long now = System.currentTimeMillis(); long currentDuration = now - startTime; long videoDuration = frame * 100000L / 3003L; if (currentDuration < videoDuration) { Thread.sleep(videoDuration - currentDuration); } now = System.currentTimeMillis(); log.info(String.format("FPS %f", 1000f * frame / (now - startTime))); } avpkt.size -= len; avpkt.data_offset += len; } consumeVideoData(frameSize); if (audioOutputData != null) { if (mLine.available() >= audioOutputData.length) { mLine.write(audioOutputData, 0, audioOutputData.length); audioOutputData = null; } } else if (audioFrameLength > 0) { if (!audioCodecInit) { audioCodec.init(audioFrameLength, audioChannels, audioChannels, 0); audioCodecInit = true; } while (audioDataOffset >= audioFrameLength) { for (int i = 0; i < audioFrameLength; i++) { mem.write8(audioInputAddr + i, (byte) audioData[i]); } int result = audioCodec.decode(audioInputAddr, audioFrameLength, audioOutputAddr); if (result < 0) { log.error(String.format("Audio decode error 0x%08X", result)); break; } else if (result == 0) { break; } else if (result > 0) { consumeAudioData(audioFrameLength); audioOutputData = new byte[audioCodec.getNumberOfSamples() * 2 * audioChannels]; for (int i = 0; i < audioOutputData.length; i++) { audioOutputData[i] = (byte) mem.read8(audioOutputAddr + i); } if (mLine.available() < audioOutputData.length) { break; } mLine.write(audioOutputData, 0, audioOutputData.length); audioOutputData = null; } } } } // while mLine.drain(); mLine.close(); } catch(Exception e) { e.printStackTrace(); } finally { try { is.close(); } catch(Exception ee) {} } // try c.avcodec_close(); c = null; picture = null; System.out.println(String.format("Stop playing video (%d frames).", frame)); return true; } }