簡體   English   中英

從mp4文件中提取旋轉元數據

[英]Extracting rotation metadata from mp4 file

我要編寫一個軟件才能從視頻生成縮略圖。 iPhone用戶可以縱向錄制視頻並將其發送給我。 當您在VLC等視頻播放器中打開此類視頻時,一切正常。 問題是,當您嘗試使用xugglerjCodec類的工具從此類視頻生成靜止幀時,它們似乎忽略了旋轉元數據。 我做了一些檢查,諸如mediainfoffmpeg類的cli工具實際上可以讀取該元信息並將其顯示給我。 我試圖遍歷Xuggler Stream屬性,以尋找可能看起來像這樣的信息-但沒有運氣。

是否可以將jCodec,Xuggler或Humble-video用於此類任務? 如果不是,是否有另一個圖書館可以報告此類元信息的存在?

xuggler和不起眼的視頻都可以獲取旋轉元數據,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視頻旋轉 ,我改變了旋轉碼;

不起眼的視頻可以從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;
 }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM