red5源码分析---12

Posted 二侠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了red5源码分析---12相关的知识,希望对你有一定的参考价值。

red5源码分析—服务器处理视频数据

接着《red5源码分析—11》,本章假设客户端发来的是视频数据,下面就分析服务器如何处理这些数据的。
根据前面几章的分析,基于mina框架,数据到达服务器后,最终会到达RTMPHandler的messageReceived函数,messageReceived定义在RTMPHandler的父类BaseRTMPHandler中,

    public void messageReceived(RTMPConnection conn, Packet packet) throws Exception {
        if (conn != null) {
            IRTMPEvent message = null;
            try {
                message = packet.getMessage();
                final Header header = packet.getHeader();
                final Number streamId = header.getStreamId();
                final Channel channel = conn.getChannel(header.getChannelId());
                final IClientStream stream = conn.getStreamById(streamId);
                conn.setStreamId(streamId);
                conn.messageReceived();
                message.setSource(conn);
                final byte headerDataType = header.getDataType();
                switch (headerDataType) {
                    case TYPE_AGGREGATE:
                    case TYPE_AUDIO_DATA:
                    case TYPE_VIDEO_DATA:
                        message.setSourceType(Constants.SOURCE_TYPE_LIVE);
                        if (stream != null) {
                            ((IEventDispatcher) stream).dispatchEvent(message);
                        }
                        break;
                    case TYPE_FLEX_SHARED_OBJECT:
                    case TYPE_SHARED_OBJECT:
                        ...
                    case TYPE_INVOKE:
                    case TYPE_FLEX_MESSAGE:
                        ...
                    case TYPE_NOTIFY: 
                    case TYPE_FLEX_STREAM_SEND:
                        ...
                    case TYPE_PING:
                        ...
                    case TYPE_BYTES_READ:
                        ...
                    case TYPE_CHUNK_SIZE:
                        ...
                    case Constants.TYPE_CLIENT_BANDWIDTH:
                        ...
                    case Constants.TYPE_SERVER_BANDWIDTH:
                        ...
                    default:

                }
                if (message instanceof Unknown) {

                }
            } catch (Throwable t) {

            }
            if (message != null) {
                message.release();
            }
        }
    }

messageReceived函数首先根据streamId获得ClientBroadcastStream,然后调用ClientBroadcastStream的dispatchEvent函数处理消息,

    public void dispatchEvent(IEvent event) {
        if (event instanceof IRTMPEvent && !closed) {
            switch (event.getType()) {
                case STREAM_CONTROL:
                case STREAM_DATA:
                    IRTMPEvent rtmpEvent;
                    try {
                        rtmpEvent = (IRTMPEvent) event;
                    } catch (ClassCastException e) {
                        return;
                    }
                    int eventTime = -1;
                    IoBuffer buf = null;
                    if (rtmpEvent instanceof IStreamData && (buf = ((IStreamData<?>) rtmpEvent).getData()) != null) {
                        bytesReceived += buf.limit();
                    }
                    IStreamCodecInfo codecInfo = getCodecInfo();
                    StreamCodecInfo info = null;
                    if (codecInfo instanceof StreamCodecInfo) {
                        info = (StreamCodecInfo) codecInfo;
                    }
                    if (rtmpEvent instanceof AudioData) {
                        ...
                    } else if (rtmpEvent instanceof VideoData) {
                        IVideoStreamCodec videoStreamCodec = null;
                        if (checkVideoCodec) {
                            videoStreamCodec = VideoCodecFactory.getVideoCodec(buf);
                            if (info != null) {
                                info.setVideoCodec(videoStreamCodec);
                            }
                            checkVideoCodec = false;
                        } else if (codecInfo != null) {
                            videoStreamCodec = codecInfo.getVideoCodec();
                        }
                        if (videoStreamCodec != null) {
                            videoStreamCodec.addData(buf.asReadOnlyBuffer());
                        }
                        if (info != null) {
                            info.setHasVideo(true);
                        }
                        eventTime = rtmpEvent.getTimestamp();
                    } else if (rtmpEvent instanceof Invoke) {
                        ...
                    } else if (rtmpEvent instanceof Notify) {
                        ...
                    }
                    if (eventTime > latestTimeStamp) {
                        latestTimeStamp = eventTime;
                    }
                    checkSendNotifications(event);
                    try {
                        if (livePipe != null) {
                            RTMPMessage msg = RTMPMessage.build(rtmpEvent, eventTime);
                            livePipe.pushMessage(msg);
                        } else {

                        }
                    } catch (IOException err) {
                        stop();
                    }
                    if (rtmpEvent instanceof IStreamPacket) {
                        for (IStreamListener listener : getStreamListeners()) {
                            try {
                                listener.packetReceived(this, (IStreamPacket) rtmpEvent);
                            } catch (Exception e) {
                                if (listener instanceof RecordingListener) {
                                    sendRecordFailedNotify(e.getMessage());
                                }
                            }
                        }
                    }
                    break;
                default:
            }
        } else {

        }
    }

dispatchEvent首先计算接收到的字节数并保存在ClientBroadcastStream的bytesReceived中;接下来通过getCodecInfo获取之前创建的StreamCodecInfo并保存在变量info中;再往下checkVideoCodec再初次接收到数据时会设置为true,因此会通过getVideoCodec创建一个IVideoStreamCodec并返回,

    public static IVideoStreamCodec getVideoCodec(IoBuffer data) {
        IVideoStreamCodec result = null;
        int codecId = data.get() & 0x0f;
        try {
            switch (codecId) {
                case 2:
                    result = (IVideoStreamCodec) Class.forName("org.red5.codec.SorensonVideo").newInstance();
                    break;
                case 3:
                    result = (IVideoStreamCodec) Class.forName("org.red5.codec.ScreenVideo").newInstance();
                    break;
                case 6:
                    result = (IVideoStreamCodec) Class.forName("org.red5.codec.ScreenVideo2").newInstance();
                    break;
                case 7:
                    result = (IVideoStreamCodec) Class.forName("org.red5.codec.AVCVideo").newInstance();
                    break;
            }
        } catch (Exception ex) {

        }
        data.rewind();
        if (result == null) {
            for (IVideoStreamCodec storedCodec : codecs) {
                IVideoStreamCodec codec;
                try {
                    codec = storedCodec.getClass().newInstance();
                } catch (Exception e) {
                    continue;
                }
                if (codec.canHandleData(data)) {
                    result = codec;
                    break;
                }
            }
        }
        return result;
    }

getVideoCodec会先检查传入的参数是否制定了codecId,如果制定了,就根据codecId创建不同的IVideoStreamCodec用于视频编解码,如果codecId未指定,就使用默认的IVideoStreamCodec并返回。

回到ClientBroadcastStream的dispatchEvent函数中,如果成功创建了IVideoStreamCodec,就将其设置进StreamCodecInfo中,这样下次就可以直接通过getVideoCodec获取到了。获得IVideoStreamCodec后,下面就调用其addData函数添加接收到的数据了,下面假设创建的IVideoStreamCodec为ScreenVideo,因此addData函数的代码如下,

    public boolean addData(IoBuffer data) {
        if (!this.canHandleData(data)) {
            return false;
        }

        data.get();
        this.updateSize(data);
        int idx = 0;
        int pos = 0;
        byte[] tmpData = new byte[this.blockDataSize];

        int countBlocks = this.blockCount;
        while (data.remaining() > 0 && countBlocks > 0) {
            short size = data.getShort();
            countBlocks--;
            if (size == 0) {
                idx += 1;
                pos += this.blockDataSize;
                continue;
            }

            this.blockSize[idx] = size;
            data.get(tmpData, 0, size);
            System.arraycopy(tmpData, 0, this.blockData, pos, size);
            idx += 1;
            pos += this.blockDataSize;
        }

        data.rewind();
        return true;
    }

addData函数首先通过canHandleData检查传入的数据属不属于ScreenVideo,这里假设通过;接下来通过updateSize函数更新本次收到的视频信息,updateSize设计到视频方面的只是,这里就不往下看了。下面就通过while循环依次取出传入的数据,一次读入一个视频block的数据,保存在其成员变量blockData中。

回到ClientBroadcastStream的dispatchEvent函数中,经过一些简单的设置后,checkSendNotifications函数什么也没做,再往下,dispatchEvent函数根据接收到的数据构造了一个RTMPMessage并调用InMemoryPushPushPipe的pushMessage函数,这里暂时不往下分析了,因为此时该InMemoryPushPushPipe并没有对应的Consumer,等后面的章节分析完了其他客户端函数后再来分析这里。
ClientBroadcastStream的dispatchEvent函数最后遍历所有的监听器,并调用其对应的packetReceived函数,根据《red5源码分析—10》的分析,这里的监听器为RecordingListener,其packetReceived函数定义如下,

    public void packetReceived(IBroadcastStream stream, IStreamPacket packet) {
        if (recording.get()) {
            CachedEvent event = new CachedEvent();
            event.setData(packet.getData().duplicate());
            event.setDataType(packet.getDataType());
            event.setReceivedTime(System.currentTimeMillis());
            event.setTimestamp(packet.getTimestamp());
            if (!queue.add(event)) {

            }
        } else {

        }
    }

根据《red5源码分析—10》的分析,这里recording.get()返回true,因此packetReceived函数接下来创建了CachedEvent用来保存刚刚收到的数据,并添加到其成员变量queue中。

在《red5源码分析—10》中分析过,RecordingListener的start函数中会创建工作线程EventQueueJob用来处理queue中的事务,下面来看,

        public void execute(ISchedulingService service) {
            if (processing.compareAndSet(false, true)) {
                try {
                    if (!queue.isEmpty()) {
                        while (!queue.isEmpty()) {
                            processQueue();
                        }
                    } else {

                    }
                } catch (Exception e) {

                } finally {
                    processing.set(false);
                }
            }
        }

其实该函数最主要的工作就是循环检查queue的内容是否为空,如果不为空就通过processQueue进行处理,processQueue函数定义在RecordingListener中,代码如下

    private void processQueue() {
        CachedEvent cachedEvent;
        try {
            IRTMPEvent event = null;
            RTMPMessage message = null;
            cachedEvent = queue.poll();
            if (cachedEvent != null) {
                final byte dataType = cachedEvent.getDataType();
                IoBuffer buffer = cachedEvent.getData();
                int bufferLimit = buffer.limit();
                if (bufferLimit > 0) {
                    switch (dataType) {
                        case Constants.TYPE_AGGREGATE:
                            event = new Aggregate(buffer);
                            event.setTimestamp(cachedEvent.getTimestamp());
                            message = RTMPMessage.build(event);
                            break;
                        case Constants.TYPE_AUDIO_DATA:
                            event = new AudioData(buffer);
                            event.setTimestamp(cachedEvent.getTimestamp());
                            message = RTMPMessage.build(event);
                            break;
                        case Constants.TYPE_VIDEO_DATA:
                            event = new VideoData(buffer);
                            event.setTimestamp(cachedEvent.getTimestamp());
                            message = RTMPMessage.build(event);
                            break;
                        default:
                            event = new Notify(buffer);
                            event.setTimestamp(cachedEvent.getTimestamp());
                            message = RTMPMessage.build(event);
                            break;
                    }
                    recordingConsumer.pushMessage(null, message);
                } else if (bufferLimit == 0 && dataType == Constants.TYPE_AUDIO_DATA) {
                    event = new AudioData(IoBuffer.allocate(0));
                    event.setTimestamp(cachedEvent.getTimestamp());
                    message = RTMPMessage.build(event);
                    recordingConsumer.pushMessage(null, message);
                } else {

                }
            }
        } catch (Exception e) {

        }
    }

processQueue函数轮询queue,查看是否有事件发生,如果有事件就根据事件的类型(本章假设为视频)创建VideoData,继而创建RTMPMessage,然后通过recordingConsumer的pushMessage函数继续发送出去,recordingConsumer的类型为FileConsumer,下面来看它的pushMessage函数,

    public void pushMessage(IPipe pipe, IMessage message) throws IOException {
        if (message instanceof RTMPMessage) {
            final IRTMPEvent msg = ((RTMPMessage) message).getBody();
            byte dataType = msg.getDataType();
            int timestamp = msg.getTimestamp();
            if (!(msg instanceof FlexStreamSend)) {
                lastTimestamp = timestamp;
            }
            if (msg instanceof VideoData) {
                if (!gotVideoKeyFrame) {
                    VideoData video = (VideoData) msg;
                    if (video.getFrameType() == FrameType.KEYFRAME) {
                        gotVideoKeyFrame = true;
                    } else {
                        return;
                    }
                }
            }
            if (writer == null) {
                init();
            }
            if (!delayWrite) {
                write(timestamp, msg);
            } else {
                ...
            }
        } else if (message instanceof ResetMessage) {
            startTimestamp = -1;
        }
    }

因为FileConsumer的成员变量delayWrite默认为false,因此这里重点看两个函数,一个是init初始化writer,另一个就是write函数将数据写入文件了。
下面先来看init函数,

    private void init() throws IOException {
        if (file != null) {
            if (delayWrite) {
                ...
            }
            IStreamableFileFactory factory = (IStreamableFileFactory) ScopeUtils.getScopeService(scope, IStreamableFileFactory.class, StreamableFileFactory.class);
            File folder = file.getParentFile();
            if (!folder.exists()) {
                if (!folder.mkdirs()) {
                    throw new IOException("Could not create parent folder");
                }
            }
            if (!file.isFile()) {
                file.createNewFile();
            } else if (!file.canWrite()) {
                throw new IOException("The file is read-only");
            }
            IStreamableFileService service = factory.getService(file);
            IStreamableFile flv = service.getStreamableFile(file);
            if (mode == null || mode.equals(IClientStream.MODE_RECORD)) {
                writer = flv.getWriter();
                if (videoConfigurationTag != null) {
                    writer.writeTag(videoConfigurationTag);
                    videoConfigurationTag = null;
                }
                if (audioConfigurationTag != null) {
                    writer.writeTag(audioConfigurationTag);
                    audioConfigurationTag = null;
                }
            } else if (mode.equals(IClientStream.MODE_APPEND)) {
                writer = flv.getAppendWriter();
            } else {
                throw new IllegalStateException(String.format("Illegal mode type: %s", mode));
            }
        } else {

        }
    }

FileConsumer的init函数首先检查可写入文件的合法性,然后就调用StreamableFileFactory的getService函数获取合适的service来读写file,StreamableFileFactory由Spring生成,默认包含了4个service,配置在red5-common.xml中,

    <bean id="streamableFileFactory" class="org.red5.server.stream.StreamableFileFactory">
        <property name="services">
            <list>
                <bean id="flvFileService" class="org.red5.server.service.flv.impl.FLVService">
                    <property name="generateMetadata" value="true"/>
                </bean>
                <bean id="mp3FileService" class="org.red5.server.service.mp3.impl.MP3Service"/>
                <bean id="mp4FileService" class="org.red5.server.service.mp4.impl.MP4Service"/>
                <bean id="m4aFileService" class="org.red5.server.service.m4a.impl.M4AService"/>
            </list>
        </property>
    </bean>

可以看到Spring自动帮助StreamableFileFactory注入了4个Service,分别是FLVService、MP3Service、MP4Service和M4AService。
再来看StreamableFileFactory的getService方法,

    public IStreamableFileService getService(File fp) {
        for (IStreamableFileService service : this.services) {
            if (service.canHandle(fp)) {
                return service;
            }
        }
        return null;
    }

这里依次取出注册在StreamableFileFactory的Service,调用每个Service的canHandle函数,查看是否可以处理对应的文件,下面以FLVService为例来看canHandle函数,定义在其父类BaseStreamableFileService中,

    public boolean canHandle(File file) {
        boolean valid = false;
        if (file.exists()) {
            String absPath = file.getAbsolutePath().toLowerCase();
            int dotIndex = absPath.lastIndexOf(‘.‘);
            if (dotIndex > -1) {
                String fileExt = absPath.substring(dotIndex);
                String[] exts = getExtension().split(",");
                for (String ext : exts) {
                    if (ext.equals(fileExt)) {
                        valid = true;
                        break;
                    }
                }
            } else {

            }
        }
        return valid;
    }

canHandle函数简单来说就是查看文件的扩展名是否在FLVService可以处理的扩展名集合中,函数中getExtension返回FLVService可以支持的拓展名集合,定义在FLVService中,代码如下

    public String getExtension() {
        return ".flv";
    }

因此,FLVService只支持拓展名为.flv的文件。

回到FileConsumer的init函数中,获得FLVService之后,就通过getStreamableFile获得对应的流文件,定义在FLVService中,代码如下,

    public IStreamableFile getStreamableFile(File file) throws IOException {
        return new FLV(file, generateMetadata);
    }

回到FileConsumer的init函数中,得到getStreamableFile函数返回的FLV对象后,假设mode为IClientStream.MODE_RECORD,因此接下来通过getWriter获得FLV对象的FLVWriter,

    public ITagWriter getWriter() throws IOException {
        if (file.exists()) {
            file.delete();
        }
        file.createNewFile();
        ITagWriter writer = new FLVWriter(file, false);
        return writer;
    }

getWriter函数根据file构造一个FLVWriter,定义在FLVWriter中,

    public FLVWriter(File file, boolean append) {
        filePath = file.getAbsolutePath();
        try {
            this.append = append;
            if (append) {
                timeOffset = FLVReader.getDuration(file);
                duration = timeOffset;
                this.dataFile = new RandomAccessFile(file, "rw");
                if (!file.exists() || !file.canRead() || !file.canWrite()) {

                } else {
                    bytesWritten = file.length();
                }
                if (duration == 0) {
                    dataFile.seek(META_POSITION);
                }
            } else {
                File dat = new File(filePath + ".ser");
                if (dat.exists()) {
                    dat.delete();
                    dat.createNewFile();
                }
                this.dataFile = new RandomAccessFile(dat, "rw");
            }
        } catch (Exception e) {

        }
    }

该函数就不细看了,就是根据传入的File构造了一个RandomAccessFile。

回到FileConsumer的init函数中,如果videoConfigurationTag不为空,下面就要通过刚刚创建的FLVWriter的writeTag函数写入videoConfigurationTag了,因为writeTag涉及到视频方面的只是,这里就不往下看了。

回到FileConsumer的pushMessage函数中,接下来就要通过write函数保存视频数据了,

    private final void write(int timestamp, IRTMPEvent msg) {
        byte dataType = msg.getDataType();
        IoBuffer data = ((IStreamData<?>) msg).getData();
        if (data != null) {
            if (startTimestamp == -1) {
                startTimestamp = timestamp;
                timestamp = 0;
            } else {
                timestamp -= startTimestamp;
            }
            ITag tag = ImmutableTag.build(dataType, timestamp, data, 0);
            if (tag.getBodySize() > 0 || dataType == ITag.TYPE_AUDIO) {
                try {
                    if (timestamp >= 0) {
                        if (!writer.writeTag(tag)) {

                        }
                    } else {

                    }
                } catch (IOException e) {

                } finally {
                    if (data != null) {
                        data.clear();
                        data.free();
                    }
                }
            }
        }
    }

FileConsumer的write首先设置视频资源的timestamp,接着通过ImmutableTag的build函数构造一个ImmutableTag,代码如下,

    public static ImmutableTag build(byte dataType, int timestamp, IoBuffer data, int previousTagSize) {
        if (data != null) {
            byte[] body = new byte[data.limit()];
            int pos = data.position();
            data.get(body);
            data.position(pos);
            return new ImmutableTag(dataType, timestamp, body, previousTagSize);
        } else {
            return new ImmutableTag(dataType, timestamp, null, previousTagSize);
        }
    }

ImmutableTag的构造函数很简单,就是简单地设置一下成员变量。
回到FileConsumer的write函数中,接下来就通过FLVWriter的writeTag函数写入数据了,代码如下,

    public boolean writeTag(ITag tag) throws IOException {
        try {
            lock.acquire();
            long prevBytesWritten = bytesWritten;
            int bodySize = tag.getBodySize();
            int previousTagSize = tag.getPreviousTagSize();
            if (previousTagSize != lastTagSize) {

            }
            if (dataFile != null) {
                byte dataType = tag.getDataType();
                IoBuffer tagBody = tag.getBody();
                if (dataType != ITag.TYPE_METADATA) {
                    long fileOffset = dataFile.getFilePointer();
                    if (fileOffset < prevBytesWritten) {
                        dataFile.seek(prevBytesWritten);
                    }
                } else {
                    tagBody.mark();
                    Input metadata = new Input(tagBody);
                    metadata.readDataType();
                    String metaType = metadata.readString();
                    try {
                        tagBody.reset();
                    } catch (InvalidMarkException e) {

                    }
                    if (!"onCuePoint".equals(metaType)) {
                        metaTags.put(System.currentTimeMillis(), tag);
                        return true;
                    }
                }
                int totalTagSize = TAG_HEADER_LENGTH + bodySize + 4;
                dataFile.setLength(dataFile.length() + totalTagSize);
                ByteBuffer tagBuffer = ByteBuffer.allocate(totalTagSize);
                int timestamp = tag.getTimestamp() + timeOffset;
                byte[] bodyBuf = null;
                if (bodySize > 0) {
                    bodyBuf = new byte[bodySize];
                    tagBody.get(bodyBuf);
                    if (dataType == ITag.TYPE_AUDIO) {
                        ...
                    } else if (dataType == ITag.TYPE_VIDEO) {
                        videoDataSize += bodySize;
                        if (videoCodecId == -1) {
                            int id = bodyBuf[0] & 0xff; // must be unsigned
                            videoCodecId = id & ITag.MASK_VIDEO_CODEC;
                        }
                    }
                }
                IOUtils.writeUnsignedByte(tagBuffer, dataType); //1
                IOUtils.writeMediumInt(tagBuffer, bodySize); //3
                IOUtils.writeExtendedMediumInt(tagBuffer, timestamp); //4
                tagBuffer.put(DEFAULT_STREAM_ID); //3
                if (bodyBuf != null) {
                    tagBuffer.put(bodyBuf);
                }
                tagBuffer.putInt(TAG_HEADER_LENGTH + bodySize);
                tagBuffer.flip();
                dataFile.write(tagBuffer.array());
                bytesWritten = dataFile.length();
                lastTagSize = TAG_HEADER_LENGTH + bodySize;
                tagBuffer.clear();
                duration = Math.max(duration, timestamp);
                if ((bytesWritten - prevBytesWritten) != totalTagSize) {

                }
                return true;
            } else {

            }
        } catch (InterruptedException e) {

        } finally {
            updateInfoFile();
            lock.release();
        }
        return false;
    }

FLVWriter的writeTag函数还比较长,涉及到一些如何保存数据的问题,这里简要分析一下这个函数。首先构造了一个缓存tagBuffer,向其中写入数据类型(即视频),数据长度,timestamp,streamId等信息,然后再向tagBuffer中写入数据,最重要的一步是通过dataFile的write函数写入tagBuffer中的数据,最后通过updateInfoFile更新文件信息,

    private void updateInfoFile() {
        RandomAccessFile infoFile = null;
        try {
            infoFile = new RandomAccessFile(filePath + ".info", "rw");
            infoFile.writeInt(audioCodecId);
            infoFile.writeInt(videoCodecId);
            infoFile.writeInt(duration);
            infoFile.writeInt(audioDataSize);
            infoFile.writeInt(soundRate);
            infoFile.writeInt(soundSize);
            infoFile.writeInt(soundType ? 1 : 0);
            infoFile.writeInt(videoDataSize);
        } catch (Exception e) {

        } finally {
            if (infoFile != null) {
                try {
                    infoFile.close();
                } catch (IOException e) {
                }
            }
        }
    }

下一章开始分析客户端如何播放服务器上的数据。

以上是关于red5源码分析---12的主要内容,如果未能解决你的问题,请参考以下文章

Android 插件化VirtualApp 源码分析 ( 目前的 API 现状 | 安装应用源码分析 | 安装按钮执行的操作 | 返回到 HomeActivity 执行的操作 )(代码片段

Android 逆向整体加固脱壳 ( DEX 优化流程分析 | DexPrepare.cpp 中 dvmOptimizeDexFile() 方法分析 | /bin/dexopt 源码分析 )(代码片段

Android 事件分发事件分发源码分析 ( Activity 中各层级的事件传递 | Activity -> PhoneWindow -> DecorView -> ViewGroup )(代码片段

开源流媒体Red5-编译和部署

轻松学习 red5 教程 像视频一样很详细还有代码直接可Copy

ubuntu16.04部署RED5流媒体服务器