关闭

RED5流媒体服务器作为客户端转发流至另一个RED5服务器

标签: RED5RED5客户端流转发RED5客户端流媒体服务器发布
1501人阅读 评论(0) 收藏 举报
分类:

RED5流媒体服务器,是Java开源的实现RTMP协议的服务器。有关RTMP协议,网上有很多的介绍。现在着重介绍一下,客户端连接RED5服务器的开发流程。

RED5有一个client包red-client.jar实现了对客户端的封装,可以用来连接RED5服务器,发布数据。

RED5 client连接成功服务器后,第一步需要创建一个流:createStream,这是,服务端会创建一个streamId返回到客户端,第二步需要publish了,发布成功后,即可发布数据(流数据)。

主要的流程是:

1、FFmpeg(或者其它推流客户端)--(流推送)--->RED5服务器、接收到流后,再连接另一台RED5

         2、当接收到流后,作为客户端进行(流转发)至另一台--->RED5服务器

        下面,主要说下客户端到服务端的开发修改原因RED5的部分:

客户端修改red-client.jar:

BaseRTMPClientHandler类,主要添加一个方法,暴露出客户端的一个connectionconsumer,

public ConnectionConsumer getConnectionConsumerByStreamId(int streamId) {
        return streamDataMap.get(streamId).connConsumer;
    }
这个方法的主要目的是:拿到客户端的消费者,注册到服务端的消费者队列中。客户端的代码修改就这么多。

服务端修改,主要是修改red-server-common.jar的内容:

StreamService类的publish方法,这个方法对应的是:客户端的publish,这时候会创建一个IClientBroadcastStream类,这个类的主要作用就是可以对流进行分发:分发到web-client的连接,把流保存成flv等,在这个publish方法中,主要是为了获取到RED5服务端的connection(这个conn主要是上面的流推送时的conn,也就是红色部分),这个connection中包含的有streamId(区别自己开发的streamId)。

ClientBroadcastStream类中,添加一个方法:

/** 获取live pipe add by zjk*/
	public IPipe getLivePipe() {
		return this.livePipe;
	}
这个方法,主要是对外提供一个获取pipe的接口,这个pipe是一个管道,可以在这个管道上注册connectionconsumer(可以把BaseRTMPClientHandler类中的connectionconsumer注册进来,这样只要服务端来了流信息,就可以自动转发到另一台服务器)。

RTMPProtocolDecoder类的decodeStreamMetadata这个方法,主要是解析onMetaData(FLV格式的元数据信息)的,这里想要说的是,发送的数据格式是以“@setDataFrame”开始的,可以在ClientBroadcastStream中拿到metaData的信息,不过,需要封装成以“@setDataFrame”开头格式的数据,要不,RED5不能解析只以onMeta开头的数据。如果不能封装的话,可以在decodeStreamMetadata方法中添加一些逻辑或者修改它的方法参数,保存到客户端(自己开发的)里面,以streamId(这个Id可以在decodeMessage方法中拿到)作为区分。
至此,RED5的原有部分已经修改完成,剩下的设计工作,则需要根据业务设计。

FFmpeg推送流到RED5服务器的流程大致如下:


RTMP连接客户端到red5服务端,其实跟FFmpeg是一样的流程,只不过,这时不需要保存一些额外的信息。


发布成功后,第一步需要做的是:发送解码数据StreamCodecInfo,如果不发送,则很有可能会出现只有声音,没有画面的情况,代码如下:


ClientBroadcastStream stream = (ClientBroadcastStream)conn.getStreamById(RTMPClient.serverStreamId);
                            ConnectionConsumer consumer = rtmpClient.getConnectionConsumerByStreamId(rtmpClient.getStreamId());
                            IStreamCodecInfo codecInfo = stream.getCodecInfo();
                            if (codecInfo instanceof StreamCodecInfo) {
                                StreamCodecInfo info = (StreamCodecInfo) codecInfo;
                                // handle video codec with configuration
                                IVideoStreamCodec videoCodec = info.getVideoCodec();
                                log.debug("Video codec: {}", videoCodec);
                                if (videoCodec != null) {
                                    // check for decoder configuration to send
                                    IoBuffer config = videoCodec.getDecoderConfiguration();
                                    if (config != null) {
                                        log.debug("Decoder configuration is available for {}", videoCodec.getName());
                                        //log.debug("Dump:\n{}", Hex.encodeHex(config.array()));
                                        VideoData conf = new VideoData(config.asReadOnlyBuffer());
                                        log.trace("Configuration ts: {}", conf.getTimestamp());
                                        RTMPMessage confMsg = RTMPMessage.build(conf);
                                        try {
                                            log.debug("Pushing video decoder configuration");
                                            consumer.pushMessage(null,confMsg);
                                        } finally {
                                            conf.release();
                                        }
                                    }
                                    // check for a keyframe to send
                                    IoBuffer keyFrame = videoCodec.getKeyframe();
                                    if (keyFrame != null) {
                                        log.debug("Keyframe is available");
                                        VideoData video = new VideoData(keyFrame.asReadOnlyBuffer());
                                        log.trace("Keyframe ts: {}", video.getTimestamp());
                                        //log.debug("Dump:\n{}", Hex.encodeHex(keyFrame.array()));
                                        RTMPMessage videoMsg = RTMPMessage.build(video);
                                        try {
                                            log.debug("Pushing keyframe");
                                            consumer.pushMessage(null,videoMsg);
                                        } finally {
                                            video.release();
                                        }
                                    }
                                } else {
                                    log.debug("No video decoder configuration available");
                                }
                                // handle audio codec with configuration
                                IAudioStreamCodec audioCodec = info.getAudioCodec();
                                log.debug("Audio codec: {}", audioCodec);
                                if (audioCodec != null) {
                                    // check for decoder configuration to send
                                    IoBuffer config = audioCodec.getDecoderConfiguration();
                                    if (config != null) {
                                        log.debug("Decoder configuration is available for {}", audioCodec.getName());
                                        //log.debug("Dump:\n{}", Hex.encodeHex(config.array()));
                                        AudioData conf = new AudioData(config.asReadOnlyBuffer());
                                        log.trace("Configuration ts: {}", conf.getTimestamp());
                                        RTMPMessage confMsg = RTMPMessage.build(conf);
                                        try {
                                            log.debug("Pushing audio decoder configuration");
                                            consumer.pushMessage(null,confMsg);
                                        } finally {
                                            conf.release();
                                        }
                                    }
                                } else {
                                    log.debug("No audio decoder configuration available");
                                }
                            }
上面的数据发送完成后,再发送onMetaData数据:

rtmpClient.publishStreamData(RTMPMessage.build(stream.getMetaData()));
或者
private void sendOnMetaData() {
        logger.error("========="+RTMPClient.ioBuffer);
        Notify metaNotify = new Notify();
//        metaNotify.setTimestamp(0);
//        IoBuffer data = IoBuffer.allocate(1024);
//        try {
//            Charset charset = Charset.defaultCharset();
//            CharsetEncoder encoder = charset.newEncoder();
//            data.putString("@setDataFrame", encoder);
//            data.putString("onMetaData", encoder);
//            data.put(RTMPClient.notify.getData());
//        } catch (Exception e) {
//            logger.error("Put the string error.",e);
//        }
<span style="white-space:pre">	</span>//这个ioBuffer就是保存的metaData数据
        metaNotify.setData(RTMPClient.ioBuffer);
        publishStreamData(streamId,RTMPMessage.build(metaNotify));
    }
发送完成后,即可注册到pipe上:

stream.getLivePipe().subscribe(consumer,null);
上面的consumer是RTMP客户端的consumer。

pushMessage方法中,会有一次发送chunkSize的包,这个主要是协调服务端跟客户端包的大小。RED5的代码默认值是1024,可以修改。

ConnectionConsumer

private int chunkSize = 128; //TODO: Not sure of the best value here


至此,开发完成,至于细节问题,则可以详细的设计。
RED5的版本是:1.0.5,而这个版本的IO包中,FLVWriter类的305行,有一个bug,在保存成流文件是,经常出错,

原来代码是:

tag.getBody().mark();
					// get input data
					Input metadata = new Input(tag.getBody());
					// initialize type so that readString knows what to do
					metadata.readDataType();
					String metaType = metadata.readString(String.class);
					log.debug("Metadata tag type: {}", metaType);
					tag.getBody().reset();
					if (!"onCuePoint".equals(metaType)) {
						// store any incoming onMetaData tags until we close the file, allow onCuePoint tags to continue
						metaTags.put(System.currentTimeMillis(), tag);
						return true;
					}
修改后的代码是:

byte dataType = tag.getDataType();
                /**
                 * when tag is ImmutableTag which is in red5-server-common.jar, tag.getBody().reset() will throw InvalidMarkException
                 * because ImmutableTag.getBody() returns a new IoBuffer instance everytime.
                 */
                IoBuffer tagBody = tag.getBody();
				// if we're writing non-meta tags do seeking and tag size update
				if (dataType != ITag.TYPE_METADATA) {
					// get the current file offset
					long fileOffset = dataFile.getFilePointer();
					log.debug("Current file offset: {} expected offset: {}", fileOffset, prevBytesWritten);
					if (fileOffset < prevBytesWritten) {
						log.debug("Seeking to expected offset");
						// it's necessary to seek to the length of the file
						// so that we can append new tags
						dataFile.seek(prevBytesWritten);
						log.debug("New file position: {}", dataFile.getChannel().position());
					}
				} else {
                    tagBody.mark();
					// get input data
					Input metadata = new Input(tagBody);
					// initialize type so that readString knows what to do
					metadata.readDataType();
					String metaType = metadata.readString(String.class);
					log.debug("Metadata tag type: {}", metaType);
                    try{
                        tagBody.reset();
                    } catch (InvalidMarkException e) {
                        //TDJ: this error is probably caused by the setter of limit on readString method
                        log.debug("Exception reseting position of buffer: " + e.getMessage(), e);
                    }
					if (!"onCuePoint".equals(metaType)) {
						// store any incoming onMetaData tags until we close the file, allow onCuePoint tags to continue
						metaTags.put(System.currentTimeMillis(), tag);
						return true;
					}
				}
				// set a var holding the entire tag size including the previous tag length
				int totalTagSize = TAG_HEADER_LENGTH + bodySize + 4;
				// resize
				dataFile.setLength(dataFile.length() + totalTagSize);
				// create a buffer for this tag
				ByteBuffer tagBuffer = ByteBuffer.allocate(totalTagSize);
				// get the timestamp
				int timestamp = tag.getTimestamp() + timeOffset;
				// allow for empty tag bodies
				byte[] bodyBuf = null;
				if (bodySize > 0) {
					// create an array big enough
					bodyBuf = new byte[bodySize];
					// put the bytes into the array
					tagBody.get(bodyBuf);

不过,在我修改这个bug后,看它的1.0.6版本,发现,已经修改了此bug。大哭


还有一种方式是:通过client连接一个red5,再连接另一个red5。用client播放一个red5的流,client在监听到流数据后,即可发布到另一台服务器,即可实现流的转发。这个是RED5的client包中已经实现的效果。StreamRelay.java中的方法。具体的可以查看red5的源码包中StreamRelay类。

0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:3129次
    • 积分:83
    • 等级:
    • 排名:千里之外
    • 原创:5篇
    • 转载:0篇
    • 译文:0篇
    • 评论:0条
    文章分类