Custom Raw Video access example

The following defines a custom filter doing raw video write access (e.g. pixel modification) and forwarding the result in the middle of the pipeline.
We cover two methods here:

- inplace processing, where the input data is modified and sent
- read access, where the output data can be anything (in this example, its is a copy of the input with a line drawn on the luma plane)

We assume numpy is available.

```

#define a custom filter
class MyFilter(gpac.FilterCustom):
    def __init__(self, session):
        gpac.FilterCustom.__init__(self, session, "PYRawVid")
        #indicate what we accept and produce - here, raw video in and out
        self.push_cap("StreamType", "Visual", gpac.GF_CAPS_INPUT_OUTPUT)
        self.push_cap("CodecID", "raw", gpac.GF_CAPS_INPUT_OUTPUT)
        self.nb_pck=0
        #change this to test no inplace access
        self.inplace=True

    #configure input PIDs
    def configure_pid(self, pid, is_remove):
        if is_remove:
            return 0
        if pid in self.ipids:
            print('PID reconfigured')
        else:
            print('New PID !')
            #create associated output PID
            opid = self.new_pid()
            pid.opid = opid

        #copy properties - this should always be done unless you have a good reason not to
        #this example assumes we keep the same pixel format
        pid.opid.copy_props(pid)

        #get width, height, stride and pixel format - get_prop may return None if property is not yet known
        #but this should not happen for these properties with raw video, except StrideUV which is None for non (semi) planar YUV formats
        self.width = pid.get_prop('Width');
        self.height = pid.get_prop('Height');
        self.pixfmt = pid.get_prop('PixelFormat');
        self.stride = pid.get_prop('Stride');
        self.stride_uv = pid.get_prop('StrideUV');
        return 0

    #process
    def process(self):
        for pid in self.ipids:
            pck = pid.get_packet()
            if pck==None:
                if pid.eos:
                    pid.opid.eos = True
                break

            tmp_pck = None
            data = None
            size = 0
            #inplace processing, we use clone regardless of whether the packet is a frame interface or not
            if self.inplace:
                #clone will handle duplication of memory if needed, cf gf_filter_pck_new_clone
                opck = pid.opid.new_pck_clone(pck)
                if opck == None:
                    raise Exception("Packet clone failed")
                opck.copy_props(pck)
                odata = opck.data
            else:
                #no inplace processing (typically the output data size differs from input, i.e. resize/plane splitter/...)
                #we need to get read-only access to the frame data
                #if packet is a frame interface (GPU texture, codec internal mem), direct access to the data from python is not possible (see next example for GPU texture access)
                #we therefore clone the packet which will read back the private data into a read/write memory, and access the new packet data as NPArray
                #we will then discard this temp packet
                #another method for cloning is shown in the video sink example below
                if pck.frame_ifce:
                    tmp_pck = pid.opid.new_pck_clone(pck)
                    if tmp_pck == None:
                        raise Exception("Packet clone failed")
                    data = tmp_pck.data
                    size = tmp_pck.size
                else:
                    #data is read-only !
                    data = pck.data
                    size = pck.size

                #create new packet - in this example we use the same output characteristics as input and copy over the data
                opck = pid.opid.new_pck(size)
                if opck == None:
                    raise Exception("Packet alloc failed")
                opck.copy_props(pck)
                odata = opck.data
                #copy array
                numpy.copyto(odata, data)


            #modify data as needed; here, we draw a line at 50% luma intensity in the middle of the luma plane of the frame
            offset = int(self.height/2 * self.stride);
            for x in range(1, self.width):
                odata[x-1 + offset] = 125;

            opck.send()

            if tmp_pck:
                tmp_pck.discard()

            #we can drop the input packet
            pid.drop_packet()
        return 0


#load a source filter
src=fs.load_src("source.mp4")

#load a custom filter
my_filter = MyFilter(fs)

#load a destination filter
dst=fs.load("vout")

#we need to indicate that our destination only gets its input from our custom filter !
dst.set_source(my_filter)