2016-05-17 89 views
0

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

有沒有可能使用jCodec,Xuggler或謙虛視頻這樣的任務?如果沒有 - 是否有另一個可以報告這些元信息存在的庫?

回答

-1

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 video rotation,我改變了旋轉代碼;

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