From 34f62a5d860521f63b8000a0f818dbcdd14d4756 Mon Sep 17 00:00:00 2001
From: Mark <mindmark@gmail.com>
Date: Sun, 6 Oct 2013 14:34:15 -0700
Subject: [PATCH 1/2] added seek module from seek branch

---
 av/seek.pxd      |  34 ++++++
 av/seek.pyx      | 297 +++++++++++++++++++++++++++++++++++++++++++++++
 examples/seek.py |  47 ++++++++
 3 files changed, 378 insertions(+)
 create mode 100644 av/seek.pxd
 create mode 100644 av/seek.pyx
 create mode 100644 examples/seek.py

diff --git a/av/seek.pxd b/av/seek.pxd
new file mode 100644
index 0000000..cdda08b
--- /dev/null
+++ b/av/seek.pxd
@@ -0,0 +1,34 @@
+from libc.stdint cimport int64_t
+from cpython cimport bool
+
+cimport av.format
+cimport av.codec
+    
+cdef class SeekContext(object):
+
+    cdef av.format.Context ctx
+    cdef av.format.Stream stream
+    cdef av.codec.Codec codec
+    
+    cdef object frame
+    
+    #state info
+
+    cdef bool frame_available
+    cdef bool seeking
+    cdef bool pts_seen
+    cdef int nb_frames
+        
+    cdef readonly int current_frame_index
+    
+    cdef int64_t current_dts
+    cdef int64_t previous_dts
+    
+    cdef flush_buffers(self)
+    cdef seek(self, int64_t timestamp, int flags)
+    
+    cpdef step_forward(self)
+
+    cpdef frame_to_ts(self, int frame)
+    cpdef ts_to_frame(self, int64_t timestamp)
+    
\ No newline at end of file
diff --git a/av/seek.pyx b/av/seek.pyx
new file mode 100644
index 0000000..6325c7b
--- /dev/null
+++ b/av/seek.pyx
@@ -0,0 +1,297 @@
+from libc.stdint cimport uint8_t, uint16_t, uint32_t, uint64_t, int64_t
+cimport libav as lib
+
+cimport av.format
+
+from .utils cimport err_check
+
+
+FIRST_FRAME_INDEX = 0
+
+class SeekError(ValueError):
+    pass
+
+class SeekEnd(SeekError):
+    pass
+    
+cdef class SeekContext(object):
+    def __init__(self,av.format.Context ctx, 
+                      av.format.Stream stream):
+        
+        self.ctx = ctx
+        self.stream = stream
+        self.codec = stream.codec
+        
+        self.frame = None
+        self.nb_frames = 0
+        
+        self.frame_available =True
+        
+        self.pts_seen = False
+        self.seeking = False
+        
+        self.current_frame_index = FIRST_FRAME_INDEX -1
+        self.current_dts = lib.AV_NOPTS_VALUE
+        self.previous_dts = lib.AV_NOPTS_VALUE
+
+    def __repr__(self):
+        return '<%s.%s curr_frame: %i curr_dts: %i prev_dts: %i key_dts: %i first_dts: %i at 0x%x>' % (
+            self.__class__.__module__,
+            self.__class__.__name__,
+            self.current_frame_index,
+            self.current_dts,
+            self.previous_dts,
+            self.keyframe_packet_dts,
+            self.first_dts,
+            id(self),
+        )
+        
+    cdef flush_buffers(self):
+        lib.avcodec_flush_buffers(self.codec.ctx)
+        
+    cdef seek(self, int64_t timestamp, int flags):
+        self.flush_buffers()
+        err_check(lib.av_seek_frame(self.ctx.proxy.ptr, self.stream.ptr.index, timestamp,flags))
+        
+    def reset(self):
+        self.seek(0,0)
+        self.frame =None
+        self.current_frame_index = FIRST_FRAME_INDEX -1
+        
+    cpdef step_forward(self):
+        cdef av.codec.Packet packet
+                
+        cdef av.codec.VideoFrame video_frame
+        
+        if not self.frame_available:
+            raise SeekEnd("No more frames")
+        
+        self.current_frame_index += 1
+        
+        #check last frame sync
+        if self.frame and not self.seeking:
+            pts = self.frame.pts
+
+            if pts != lib.AV_NOPTS_VALUE:
+                pts_frame_num = self.ts_to_frame(pts)
+                
+                if self.current_frame_index -1 < pts_frame_num:
+                    #print  "dup frame",self.current_frame_index, "!=",self.ts_to_frame(pts)
+                    video_frame = self.frame
+                    video_frame.frame_index = self.current_frame_index
+                    return video_frame
+
+        while True:
+
+            packet = next(self.ctx.demux([self.stream]))
+            
+            if packet.struct.pts != lib.AV_NOPTS_VALUE:
+                self.pts_seen = True
+            
+            frame = self.stream.decode(packet)
+            if frame:
+                
+                #check sync to see if we need to drop the frame
+                if not self.seeking:
+                    pts = frame.pts
+                    
+                    if pts != lib.AV_NOPTS_VALUE:
+                        
+                        pts_frame_num = self.ts_to_frame(pts)
+                        #print self.current_frame_index,pts_frame_num
+                        #allow one frame error mkv off by pts ?!!! 
+                        if self.current_frame_index > pts_frame_num + 1:
+                            print "need drop frame out of sync", self.current_frame_index, ">",self.ts_to_frame(pts)
+                            continue
+                            #raise Exception()
+
+                video_frame = frame
+                video_frame.frame_index = self.current_frame_index
+                    
+                self.frame = video_frame
+                return video_frame
+            else:
+                if packet.is_null:
+                    self.frame_available = False
+                    raise SeekEnd("No more frames")
+            
+    
+    def __getitem__(self,x):
+
+        return self.to_frame(x)
+    
+    def __len__(self):
+        if not self.nb_frames:
+            
+            if self.stream.frames:
+                self.nb_frames = self.stream.frames
+            else:
+                self.nb_frames = self.get_length_seek()
+            
+        return self.nb_frames
+    
+    
+    def get_length_seek(self):
+        """Get the last frame by seeking to the end of the stream. returns length
+        """
+        
+        cur_frame = self.current_frame_index
+        if cur_frame <0:
+            cur_frame = 0
+        
+        cdef lib.AVRational stream_time_base
+        
+        duration = self.stream.duration
+        
+        # If the stream doesn't have a duration use duration of av.format.Context
+        # and convert it to stream timebase
+        
+        if duration == lib.AV_NOPTS_VALUE:
+            
+            ctx_duration = self.ctx.duration
+            time_base = self.stream.time_base
+            
+            stream_time_base.num = time_base.numerator
+            stream_time_base.den = time_base.denominator
+            
+            duration = lib.av_rescale_q(ctx_duration,
+                                        lib.AV_TIME_BASE_Q, 
+                                        stream_time_base)
+        
+
+        last_frame = self.ts_to_frame(duration + self.stream.start_time)
+        self.to_nearest_keyframe(last_frame)
+
+        while True:
+            try:
+                frame = self.step_forward()
+            except SeekEnd as e:
+                break
+            
+        length =  self.current_frame_index
+        self.to_frame(cur_frame)
+
+        return length
+
+    def to_frame(self, int target_frame):
+        
+        """Seek to frame and return it
+        """
+        
+        # seek to the nearet keyframe
+        self.to_nearest_keyframe(target_frame)
+        
+        if target_frame == self.current_frame_index:
+            return self.frame
+
+        # something went wrong 
+        if self.current_frame_index > target_frame:
+            self.to_nearest_keyframe(target_frame-1)
+            #raise IndexError("error advancing to key frame before seek (index isn't right)")
+        
+        frame = self.frame
+        
+        # step step_forward from current frame until we get to the frame
+        while self.current_frame_index < target_frame:
+            frame = self.step_forward()
+
+        return self.frame
+    
+
+    def to_nearest_keyframe(self,int target_frame,offset=0):
+        
+        """Seek to as close as the target frame as possible without additional frame decoding.
+        Sometimes seeking will go too far (current frame > targer_frame), thats what offset is for.
+        The offset arg will try seeking too target_frame - offset, while still trying to get the 
+        current frame <= target_frame.
+        
+        """
+        
+        #optimizations
+        if not self.seeking:
+            if target_frame == self.current_frame_index:
+                return self.frame
+            
+            if target_frame == self.current_frame_index + 1:
+                return self.step_forward()
+
+        if target_frame - offset < 0:
+            raise SeekError("cannot seek before first frame")
+        
+        cdef int flags = 0
+        cdef int64_t seek_pts = lib.AV_NOPTS_VALUE
+        cdef int64_t current_pts = lib.AV_NOPTS_VALUE
+        
+        self.seeking = True
+        self.frame_available = True
+        self.current_frame_index = -2
+        
+        seek_ts  = self.frame_to_ts(target_frame - offset)
+        
+        flags = lib.AVSEEK_FLAG_BACKWARD 
+        
+        self.seek(seek_ts,flags)
+        
+        retry = 10
+        
+        # Keep stepping forward until we find a valid pts. Seek should land 
+        # on a key frame and the next decoded frame should have a valid pts
+        # a retry limit is here just in case so we don't end up decoding every frame.
+        
+        while current_pts == lib.AV_NOPTS_VALUE:
+            frame  = self.step_forward()
+            current_pts = frame.pts
+            retry -= 1
+            if retry < 0:
+                raise SeekError("Connnot find keyframe %i %i" % (seek_pts, target_frame) )
+            
+        current_frame = self.ts_to_frame(current_pts)
+        
+        #if we seek too far increment the offset and try seeking again  
+        if current_frame > target_frame:
+            print "seeked too far trying again with offset"
+            print  "offset=%i current_frame=%i target_frame=%i seek_target=%i" % (offset, current_frame,target_frame, target_frame- offset)
+            return self.to_nearest_keyframe(target_frame, offset + 1)
+            
+        self.current_frame_index = self.ts_to_frame(current_pts)
+        
+        cdef av.codec.VideoFrame video_frame
+        
+        video_frame = self.frame
+        video_frame.frame_index = self.current_frame_index
+
+        self.seeking = False
+        return video_frame
+
+    cpdef frame_to_ts(self, int frame):
+    
+        """convert frame number to time stamp using stream time base
+        """
+        
+        fps = self.stream.base_frame_rate
+        time_base = self.stream.time_base
+        
+        cdef int64_t pts
+        
+        pts = self.stream.start_time + ((frame * fps.denominator * time_base.denominator) \
+                                 / (fps.numerator *time_base.numerator))
+
+        return pts
+    
+    cpdef ts_to_frame(self, int64_t timestamp):
+    
+        """convert time stamp to frame number using streams time base
+        """
+        
+        if timestamp == lib.AV_NOPTS_VALUE:
+            raise Exception("time stamp AV_NOPTS_VALUE")
+        
+        fps = self.stream.base_frame_rate
+        time_base = self.stream.time_base
+        
+        cdef int64_t frame
+        
+        frame = ((timestamp - self.stream.start_time) * time_base.numerator * fps.numerator) \
+                                      / (time_base.denominator * fps.denominator)
+                                      
+        return frame
\ No newline at end of file
diff --git a/examples/seek.py b/examples/seek.py
new file mode 100644
index 0000000..0e1ce69
--- /dev/null
+++ b/examples/seek.py
@@ -0,0 +1,47 @@
+"""
+randomly save out frames of video
+"""
+
+import os
+import sys
+import random
+import Image
+
+from av import open
+from av.seek import SeekContext
+
+video = open(sys.argv[1])
+
+streams = [s for s in video.streams if s.type == b'video']
+streams = [streams[0]]
+
+stream = streams[0]
+
+
+frame_count = 0
+
+
+seek = SeekContext(video,stream)
+
+
+frames = len(seek) 
+
+print "frames =", frames
+shuff = range(10)
+random.shuffle(shuff)
+
+for i,x in enumerate(shuff):
+
+    frame = seek[x]
+
+    frame_nb = frame.frame_index
+    
+    path = 'sandbox/%s.%08d.jpg' % ("seek", frame_nb)
+    
+    assert x == frame_nb
+    
+    print i,frames, frame_nb, path
+    img = Image.frombuffer("RGBA", (frame.width, frame.height), frame.to_rgba(), "raw", "RGBA", 0, 1)
+    img.save(path)
+
+
-- 
GitLab


From ad0d586354dc67c4cb4e0527813139f7c81c4125 Mon Sep 17 00:00:00 2001
From: Mark <mindmark@gmail.com>
Date: Fri, 25 Oct 2013 14:56:46 -0700
Subject: [PATCH 2/2] travis test

---
 .travis.yml | 1 +
 1 file changed, 1 insertion(+)

diff --git a/.travis.yml b/.travis.yml
index dafd666..9b58161 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -6,6 +6,7 @@ python:
 
 # Install FFmpeg.
 before_install: 
+    - sudo apt-get update
     - sudo apt-get build-dep -qq ffmpeg
     - wget http://ffmpeg.org/releases/ffmpeg-1.2.2.tar.bz2
     - tar -xf ffmpeg-1.2.2.tar.bz2
-- 
GitLab