package org.jcodec.movtool.streaming.tracks.avc; import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.List; import org.jcodec.codecs.h264.H264Decoder; import org.jcodec.codecs.h264.H264Encoder; import org.jcodec.codecs.h264.H264Utils; import org.jcodec.codecs.h264.H264Utils.SliceHeaderTweaker; import org.jcodec.codecs.h264.encode.ConstantRateControl; import org.jcodec.codecs.h264.io.model.Frame; import org.jcodec.codecs.h264.io.model.NALUnit; import org.jcodec.codecs.h264.io.model.NALUnitType; import org.jcodec.codecs.h264.io.model.PictureParameterSet; import org.jcodec.codecs.h264.io.model.SeqParameterSet; import org.jcodec.codecs.h264.io.model.SliceHeader; import org.jcodec.codecs.h264.mp4.AvcCBox; import org.jcodec.common.NIOUtils; import org.jcodec.common.model.ColorSpace; import org.jcodec.common.model.Picture; import org.jcodec.containers.mp4.MP4Util; import org.jcodec.containers.mp4.boxes.SampleDescriptionBox; import org.jcodec.containers.mp4.boxes.SampleEntry; import org.jcodec.containers.mp4.boxes.VideoSampleEntry; import org.jcodec.movtool.streaming.VirtualPacket; import org.jcodec.movtool.streaming.VirtualTrack; import org.jcodec.movtool.streaming.tracks.ClipTrack; import org.jcodec.movtool.streaming.tracks.VirtualPacketWrapper; /** * This class is part of JCodec ( www.jcodec.org ) This software is distributed * under FreeBSD License * * Clips AVC track replacing the remainder of a GOP at cut point with I-frames * * @author The JCodec project * */ public class AVCClipTrack extends ClipTrack { private AvcCBox avcC; private ConstantRateControl rc; private int mbW; private int mbH; private VideoSampleEntry se; private int frameSize; private SeqParameterSet encSPS; private PictureParameterSet encPPS; public AVCClipTrack(VirtualTrack src, int frameFrom, int frameTo) { super(src, frameFrom, frameTo); SampleEntry origSE = src.getSampleEntry(); if (!"avc1".equals(origSE.getFourcc())) throw new RuntimeException("Not an AVC source track"); rc = new ConstantRateControl(1024); H264Encoder encoder = new H264Encoder(rc); avcC = H264Utils.parseAVCC((VideoSampleEntry) origSE); SeqParameterSet sps = H264Utils.readSPS(NIOUtils.duplicate(avcC.getSpsList().get(0))); mbW = sps.pic_width_in_mbs_minus1 + 1; mbH = H264Utils.getPicHeightInMbs(sps); encSPS = encoder.initSPS(H264Utils.getPicSize(sps)); encSPS.seq_parameter_set_id = 1; encPPS = encoder.initPPS(); encPPS.seq_parameter_set_id = 1; encPPS.pic_parameter_set_id = 1; encSPS.profile_idc = sps.profile_idc; encSPS.level_idc = sps.level_idc; encSPS.frame_mbs_only_flag = sps.frame_mbs_only_flag; encSPS.frame_crop_bottom_offset = sps.frame_crop_bottom_offset; encSPS.frame_crop_left_offset = sps.frame_crop_left_offset; encSPS.frame_crop_right_offset = sps.frame_crop_right_offset; encSPS.frame_crop_top_offset = sps.frame_crop_top_offset; encSPS.vuiParams = sps.vuiParams; avcC.getSpsList().add(H264Utils.writeSPS(encSPS, 128)); avcC.getPpsList().add(H264Utils.writePPS(encPPS, 20)); se = (VideoSampleEntry) MP4Util.cloneBox(origSE, 2048, SampleDescriptionBox.FACTORY); se.removeChildren("avcC"); se.add(avcC); frameSize = rc.calcFrameSize(mbW * mbH); frameSize += frameSize >> 4; } protected List<VirtualPacket> getGop(VirtualTrack src, int from) throws IOException { VirtualPacket packet = src.nextPacket(); List<VirtualPacket> head = new ArrayList<VirtualPacket>(); while (packet != null && packet.getFrameNo() < from) { if (packet.isKeyframe()) head.clear(); head.add(packet); packet = src.nextPacket(); } List<VirtualPacket> tail = new ArrayList<VirtualPacket>(); while (packet != null && !packet.isKeyframe()) { tail.add(packet); packet = src.nextPacket(); } List<VirtualPacket> gop = new ArrayList<VirtualPacket>(); GopTranscoder tr = new GopTranscoder(head, tail); for (int i = 0; i < tail.size(); i++) gop.add(new TranscodePacket(tail.get(i), tr, i)); gop.add(packet); return gop; } public class GopTranscoder { private List<VirtualPacket> tail; private List<VirtualPacket> head; private List<ByteBuffer> result; public GopTranscoder(List<VirtualPacket> head, List<VirtualPacket> tail) { this.head = head; this.tail = tail; } public List<ByteBuffer> transcode() throws IOException { H264Decoder decoder = new H264Decoder(); decoder.addSps(avcC.getSpsList()); decoder.addPps(avcC.getPpsList()); Picture buf = Picture.create(mbW << 4, mbH << 4, ColorSpace.YUV420); Frame dec = null; for (VirtualPacket virtualPacket : head) { dec = decoder.decodeFrame(H264Utils.splitMOVPacket(virtualPacket.getData(), avcC), buf.getData()); } H264Encoder encoder = new H264Encoder(rc); ByteBuffer tmp = ByteBuffer.allocate(frameSize); List<ByteBuffer> result = new ArrayList<ByteBuffer>(); for (VirtualPacket pkt : tail) { dec = decoder.decodeFrame(H264Utils.splitMOVPacket(pkt.getData(), avcC), buf.getData()); tmp.clear(); ByteBuffer res = encoder.encodeFrame(dec, tmp); ByteBuffer out = ByteBuffer.allocate(frameSize); processFrame(res, out); result.add(out); } return result; } private void processFrame(ByteBuffer in, ByteBuffer out) { SliceHeaderTweaker st = new H264Utils.SliceHeaderTweaker() { @Override protected void tweak(SliceHeader sh) { sh.pic_parameter_set_id = 1; } }; ByteBuffer dup = in.duplicate(); while (dup.hasRemaining()) { ByteBuffer buf = H264Utils.nextNALUnit(dup); if (buf == null) break; NALUnit nu = NALUnit.read(buf); if (nu.type == NALUnitType.IDR_SLICE) { ByteBuffer sp = out.duplicate(); out.putInt(0); nu.write(out); st.run(buf, out, nu, encSPS, encPPS); sp.putInt(out.position() - sp.position() - 4); } } if (out.remaining() >= 5) { out.putInt(out.remaining() - 4); new NALUnit(NALUnitType.FILLER_DATA, 0).write(out); } out.clear(); } public synchronized List<ByteBuffer> getResult() throws IOException { if (result == null) result = transcode(); return result; } } @Override public SampleEntry getSampleEntry() { return se; } public class TranscodePacket extends VirtualPacketWrapper { private GopTranscoder tr; private int off; public TranscodePacket(VirtualPacket src, GopTranscoder tr, int off) { super(src); this.tr = tr; this.off = off; } @Override public ByteBuffer getData() throws IOException { return NIOUtils.duplicate(tr.getResult().get(off)); } @Override public int getDataLen() throws IOException { return frameSize; } @Override public boolean isKeyframe() { return true; } } }