从mp4文件

时间:2016-05-17 13:29:09

标签: java video metadata xuggler jcodec

我正在编写的一个软件需要从视频中生成缩略图。 iPhone用户可以以纵向模式录制视频并将其发送给我。 当您在VLC等视频播放器中打开此类视频时 - 一切正常。问题是当您尝试使用xugglerjCodec等工具从此类视频生成静态帧时 - 它们似乎忽略了旋转元数据。我做了一些检查,像mediainfoffmpeg这样的cli工具实际上可以读取该元信息并向我显示。我试图在Xuggler中迭代Stream属性来寻找可能看起来像这样的信息 - 没有运气。

是否有可能使用jCodec,Xuggler或Humble-video进行此类任务?如果没有 - 是否有另一个库可以报告这种元信息的存在?

1 个答案:

答案 0 :(得分:-1)

xuggler和humble-video都可以获得旋转元数据,xuggler你可以看到下面的代码:

public class MultimediaContentConverterVideo {
    public void convertOriginal(String urlIn, String urlOut, boolean debug) throws IOException {

        String workingPath = FilenameUtils.getFullPath(urlIn);
        String filenamePrefix = FilenameUtils.getBaseName(urlIn);

        // create a media reader
        IMediaReader reader = ToolFactory.makeReader(urlIn);

        // stipulate that we want BufferedImages created in BGR 24bit color space
        reader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);

        // create a writer which receives the decoded media from
        // reader, encodes it and writes it out to the specified file
        IMediaWriter writer = ToolFactory.makeWriter(urlOut, reader);

        // add a debug listener to the writer to see media writer events
        if (debug) {
            writer.addListener(ToolFactory.makeDebugListener());
        }

        // read and decode packets from the source file and
        // then encode and write out data to the output file
        VideoRotator rotator = new VideoRotator();
        reader.addListener(rotator);
        rotator.addListener(writer);

        while (reader.readPacket() == null);

    }

    private class VideoRotator extends MediaToolAdapter {

        private int rotate = 0;

        @Override
        public void onVideoPicture(IVideoPictureEvent event) {
            BufferedImage img = event.getImage();
            rotateImage(rotate, img);
            super.onVideoPicture(event);
        }
        private static BufferedImage rotateImage(int rotate, BufferedImage img) {
            if (rotate == 0 || img == null) {
                return img;
            }
            int width = img.getWidth();
            int height = img.getHeight();
            int new_w = 0, new_h = 0;
            int new_radian = rotate;
            if (rotate <= 90) {
                new_w = (int)(width * Math.cos(Math.toRadians(new_radian)) + height * Math.sin(Math.toRadians(new_radian)));
                new_h = (int)(height * Math.cos(Math.toRadians(new_radian)) + width * Math.sin(Math.toRadians(new_radian)));
            } else if (rotate <= 180) {
                new_radian = rotate - 90;
                new_w = (int)(height * Math.cos(Math.toRadians(new_radian)) + width * Math.sin(Math.toRadians(new_radian)));
                new_h = (int)(width * Math.cos(Math.toRadians(new_radian)) + height * Math.sin(Math.toRadians(new_radian)));
            } else if (rotate <= 270) {
                new_radian = rotate - 180;
                new_w = (int)(width * Math.cos(Math.toRadians(new_radian)) + height * Math.sin(Math.toRadians(new_radian)));
                new_h = (int)(height * Math.cos(Math.toRadians(new_radian)) + width * Math.sin(Math.toRadians(new_radian)));
            } else {
                new_radian = rotate - 270;
                new_w = (int)(height * Math.cos(Math.toRadians(new_radian)) +
                    width * Math.sin(Math.toRadians(new_radian)));
                new_h = (int)(width * Math.cos(Math.toRadians(new_radian)) +
                    height * Math.sin(Math.toRadians(new_radian)));
            }
            BufferedImage toStore = new
            BufferedImage(new_w, new_h, BufferedImage.TYPE_INT_RGB);
            Graphics2D g = toStore.createGraphics();
            AffineTransform affineTransform = new AffineTransform();
            affineTransform.rotate(Math.toRadians(rotate), width / 2, height / 2);
            if (rotate != 180) {
                AffineTransform translationTransform =
                    findTranslation(affineTransform, img, rotate);
                affineTransform.preConcatenate(translationTransform);
            }
            g.setColor(Color.WHITE);
            g.fillRect(0, 0, new_w, new_h);
            g.setRenderingHint(RenderingHints.KEY_INTERPOLATION,
                RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g.drawRenderedImage(img, affineTransform);
            g.dispose();
            return toStore;
        }

        private static AffineTransform findTranslation(AffineTransform at,
            BufferedImage bi, int angle) { //45
            Point2D p2din, p2dout;
            double ytrans = 0.0, xtrans = 0.0;
            if (angle <= 90) {
                p2din = new Point2D.Double(0.0, 0.0);
                p2dout = at.transform(p2din, null);
                ytrans = p2dout.getY();

                p2din = new Point2D.Double(0, bi.getHeight());
                p2dout = at.transform(p2din, null);
                xtrans = p2dout.getX();
            }
            /*else if(angle<=135){
                p2din = new Point2D.Double(0.0, bi.getHeight());
                p2dout = at.transform(p2din, null);
                ytrans = p2dout.getY();

                p2din = new Point2D.Double(bi.getWidth(),bi.getHeight());
                p2dout = at.transform(p2din, null);
                xtrans = p2dout.getX();

            }*/
            else if (angle <= 180) {
                p2din = new Point2D.Double(0.0, bi.getHeight());
                p2dout = at.transform(p2din, null);
                ytrans = p2dout.getY();

                p2din = new Point2D.Double(bi.getWidth(), bi.getHeight());
                p2dout = at.transform(p2din, null);
                xtrans = p2dout.getX();

            }
            /*else if(angle<=225){
                p2din = new Point2D.Double(bi.getWidth(), bi.getHeight());
                p2dout = at.transform(p2din, null);
                ytrans = p2dout.getY();

                p2din = new Point2D.Double(bi.getWidth(),0.0);
                p2dout = at.transform(p2din, null);
                xtrans = p2dout.getX();

            }*/
            else if (angle <= 270) {
                p2din = new Point2D.Double(bi.getWidth(), bi.getHeight());
                p2dout = at.transform(p2din, null);
                ytrans = p2dout.getY();

                p2din = new Point2D.Double(bi.getWidth(), 0.0);
                p2dout = at.transform(p2din, null);
                xtrans = p2dout.getX();

            } else {
                p2din = new Point2D.Double(bi.getWidth(), 0.0);
                p2dout = at.transform(p2din, null);
                ytrans = p2dout.getY();


                p2din = new Point2D.Double(0.0, 0.0);
                p2dout = at.transform(p2din, null);
                xtrans = p2dout.getX();

            }
            AffineTransform tat = new AffineTransform();
            tat.translate(-xtrans, -ytrans);
            return tat;
        }

        @Override
        public void onAddStream(IAddStreamEvent event) {
            int streamIndex = event.getStreamIndex();
            IStream stream = event.getSource().getContainer().getStream(streamIndex);
            IStreamCoder streamCoder = event.getSource().getContainer().getStream(streamIndex).getStreamCoder();
            if (streamCoder.getCodecType() == ICodec.Type.CODEC_TYPE_AUDIO) {
                streamCoder.setSampleRate(44100);
            } else if (streamCoder.getCodecType() == ICodec.Type.CODEC_TYPE_VIDEO) {
                String metaRotate = stream.getMetaData().getValue(META_KEY_ROTATE);
                if (metaRotate != null && metaRotate.matches("\\d+")) {
                    rotate = Integer.valueOf(metaRotate);
                }
            }
            super.onAddStream(event);
        }
    }
}

,这里的原始代码Xuggler, iPhone / iPad video rotation,我更改了旋转代码;

humble-video可以从DemuxerStream类获取旋转元数据,请参阅下面的代码:

public class HumbleVideoHelper {
 private static String META_KEY_ROTATE = "rotate";

 public static class VideoInfo {
     private Long fileSize;
     private Integer frameWidth;
     private Integer frameHeight;
     private Long duration;
     private BufferedImage firstFrameImage;
     private int rotation;

     public Long getFileSize() {
         return fileSize;
     }

     public void setFileSize(Long fileSize) {
         this.fileSize = fileSize;
     }

     public Integer getFrameWidth() {
         return frameWidth;
     }

     public void setFrameWidth(Integer frameWidth) {
         this.frameWidth = frameWidth;
     }

     public Integer getFrameHeight() {
         return frameHeight;
     }

     public void setFrameHeight(Integer frameHeight) {
         this.frameHeight = frameHeight;
     }

     public Long getDuration() {
         return duration;
     }

     public void setDuration(Long duration) {
         this.duration = duration;
     }

     public BufferedImage getFirstFrameImage() {
         return firstFrameImage;
     }

     public void setFirstFrameImage(BufferedImage firstFrameImage) {
         this.firstFrameImage = firstFrameImage;
     }

     public int getRotation() {
         return rotation;
     }

     public void setRotation(int rotation) {
         this.rotation = rotation;
     }

     @Override
     public String toString() {
         return "VideoInfo{" +
             "fileSize=" + fileSize +
             ", frameWidth=" + frameWidth +
             ", frameHeight=" + frameHeight +
             ", duration=" + duration +
             ", firstFrameImage=" + firstFrameImage +
             ", rotation=" + rotation +
             '}';
     }
 }

 private String url;

 private VideoInfo videoInfo;

 private Demuxer demuxer;

 private HumbleVideoHelper(String url) {
     this.url = url;
 }

 public static HumbleVideoHelper with(String url) {
     return new HumbleVideoHelper(url);
 }

 private void init() throws IOException, InterruptedException {
     if (demuxer == null) {
         demuxer = Demuxer.make();
         demuxer.open(url, null, false,
             true, null, null);
     }
 }

 public VideoInfo parse(boolean closeAfterParse) throws IOException, InterruptedException {
     if (videoInfo != null) {
         return videoInfo;
     }
     init();
     /*
      * Iterate through the streams to find the first video stream
      */
     int videoStreamId = -1;
     long streamStartTime = Global.NO_PTS;
     Decoder videoDecoder = null;
     DemuxerStream demuxerStream = getVideoDemuxerStream(demuxer);
     if (demuxerStream != null) {
         videoStreamId = demuxerStream.getIndex();
         streamStartTime = demuxerStream.getStartTime();
         videoDecoder = demuxerStream.getDecoder();
     }
     if (videoStreamId == -1) {
         throw new RuntimeException("could not find video stream in container: " + url);
     }
     BufferedImage image = getFirstFrameBufferedImage(demuxer, videoStreamId, streamStartTime, videoDecoder);

     videoInfo = new VideoInfo();
     setVideoInfoDuration(videoInfo);
     videoInfo.setFileSize(demuxer.getFileSize());
     videoInfo.setFrameHeight(videoDecoder.getHeight());
     videoInfo.setFrameWidth(videoDecoder.getWidth());
     videoInfo.setFirstFrameImage(image);
     videoInfo.setRotation(NumberUtils.toInt(demuxerStream.getMetaData().getValue(META_KEY_ROTATE)));
     if (closeAfterParse) {
         close();
     }
     return videoInfo;
 }

 private void setVideoInfoDuration(VideoInfo videoInfo) {
     videoInfo.setDuration((long)(demuxer.getDuration() * 1000.0 / Global.DEFAULT_PTS_PER_SECOND));
 }

 private static BufferedImage getFirstFrameBufferedImage(Demuxer demuxer, int videoStreamId, long streamStartTime,
     Decoder videoDecoder)
 throws InterruptedException, IOException {
     /*
      * Now we have found the audio stream in this file.  Let's open up our decoder so it can
      * do work.
      */
     videoDecoder.open(null, null);
     final MediaPicture picture = MediaPicture.make(
         videoDecoder.getWidth(),
         videoDecoder.getHeight(),
         videoDecoder.getPixelFormat());

     /* A converter object we'll use to convert the picture in the video to a BGR_24 format that Java Swing
       can work with. You can still access the data directly in the MediaPicture if you prefer, but this
       abstracts away from this demo most of that byte-conversion work. Go read the source code for the
       converters if you're a glutton for punishment.
      */
     final MediaPictureConverter converter =
         MediaPictureConverterFactory.createConverter(
             MediaPictureConverterFactory.HUMBLE_BGR_24,
             picture);
     BufferedImage image = null;

     // Calculate the time BEFORE we start playing.
     long systemStartTime = System.nanoTime();
     // Set units for the system time, which because we used System.nanoTime will be in nanoseconds.
     final Rational systemTimeBase = Rational.make(1, 1000000000);
     // All the MediaPicture objects decoded from the videoDecoder will share this timebase.
     final Rational streamTimebase = videoDecoder.getTimeBase();

     /*
       Now, we start walking through the container looking at each packet. This
       is a decoding loop, and as you work with Humble you'll write a lot
       of these.

       Notice how in this loop we reuse all of our objects to avoid
       reallocating them. Each call to Humble resets objects to avoid
       unnecessary reallocation.
      */
     final MediaPacket packet = MediaPacket.make();
     while (demuxer.read(packet) >= 0) {
         /*
           Now we have a packet, let's see if it belongs to our video stream
          */
         if (packet.getStreamIndex() == videoStreamId) {
             /*
               A packet can actually contain multiple sets of samples (or frames of samples
               in decoding speak).  So, we may need to call decode  multiple
               times at different offsets in the packet's data.  We capture that here.
              */
             int offset = 0;
             int bytesRead = 0;
             do {
                 bytesRead += videoDecoder.decode(picture, packet, offset);
                 if (picture.isComplete()) {
                     image = getVideoImageAtCorrectTime(streamStartTime, picture,
                         converter, image, systemStartTime, systemTimeBase,
                         streamTimebase);
                 }
                 offset += bytesRead;
                 if (image != null) {
                     break;
                 }
             } while (offset < packet.getSize());
         }
     }
     // Some video decoders (especially advanced ones) will cache
     // video data before they begin decoding, so when you are done you need
     // to flush them. The convention to flush Encoders or Decoders in Humble Video
     // is to keep passing in null until incomplete samples or packets are returned.
     do {
         if (image != null) {
             break;
         }
         videoDecoder.decode(picture, null, 0);
         if (picture.isComplete()) {
             image = getVideoImageAtCorrectTime(streamStartTime, picture, converter,
                 null, systemStartTime, systemTimeBase, streamTimebase);
         }
     } while (picture.isComplete());
     return image;
 }

 public VideoInfo parseDurationAndSize(boolean closeAfterParse) throws IOException, InterruptedException {
     init();
     VideoInfo videoInfo = new VideoInfo();
     videoInfo.setFileSize(demuxer.getFileSize());
     setVideoInfoDuration(videoInfo);
     if (closeAfterParse) {
         close();
     }
     return videoInfo;
 }

 public VideoInfo parseWithoutImage(boolean closeAfterParse) throws IOException, InterruptedException {
     init();
     /*
      * Iterate through the streams to find the first video stream
      */
     int videoStreamId = -1;
     Decoder videoDecoder = null;
     DemuxerStream demuxerStream = getVideoDemuxerStream(demuxer);
     if (demuxerStream != null) {
         videoStreamId = demuxerStream.getIndex();
         videoDecoder = demuxerStream.getDecoder();
     }
     if (videoStreamId == -1) {
         throw new RuntimeException("could not find video stream in container: " + url);
     }
     VideoInfo videoInfo = new VideoInfo();
     setVideoInfoDuration(videoInfo);
     videoInfo.setFileSize(demuxer.getFileSize());
     videoInfo.setFrameHeight(videoDecoder.getHeight());
     videoInfo.setFrameWidth(videoDecoder.getWidth());
     if (closeAfterParse) {
         close();
     }
     return videoInfo;
 }

 private static DemuxerStream getVideoDemuxerStream(Demuxer demuxer)
 throws IOException, InterruptedException {
     /*
      * Query how many streams the call to open found
      */
     int numStreams = demuxer.getNumStreams();
     for (int i = 0; i < numStreams; i++) {
         final DemuxerStream stream = demuxer.getStream(i);
         final Decoder decoder = stream.getDecoder();
         if (decoder != null && decoder.getCodecType() == MediaDescriptor.Type.MEDIA_VIDEO) {
             return stream;
         }
     }
     return null;
 }

 /**
  * Takes the video picture and displays it at the right time.
  */
 private static BufferedImage getVideoImageAtCorrectTime(long streamStartTime,
     final MediaPicture picture,
     final MediaPictureConverter converter,
     BufferedImage image, long systemStartTime,
     final Rational systemTimeBase,
     final Rational streamTimebase)
 throws InterruptedException {
     long streamTimestamp = picture.getTimeStamp();
     // convert streamTimestamp into system units (i.e. nano-seconds)
     streamTimestamp = systemTimeBase.rescale(streamTimestamp - streamStartTime, streamTimebase);
     // get the current clock time, with our most accurate clock
     long systemTimestamp = System.nanoTime();
     // loop in a sleeping loop until we're within 1 ms of the time for that video frame.
     // a real video player needs to be much more sophisticated than this.
     while (streamTimestamp > (systemTimestamp - systemStartTime + 1000000)) {
         Thread.sleep(1);
         systemTimestamp = System.nanoTime();
     }
     // finally, convert the image from Humble format into Java images.
     image = converter.toImage(image, picture);
     // And ask the UI thread to repaint with the new image.
     //    window.setImage(image);
     return image;
 }

 /**
  * It is good practice to close demuxers when you're done to free
  * up file handles. Humble will EVENTUALLY detect if nothing else
  * references this demuxer and close it then, but get in the habit
  * of cleaning up after yourself, and your future girlfriend/boyfriend
  * will appreciate it.
  */
 public void close() {
     if (demuxer != null) {
         try {
             demuxer.close();
         } catch (InterruptedException | IOException e) {
             e.printStackTrace();
         }
     }
     demuxer = null;
 }
}