接收和播放RTP媒体流
JMF Players和Processors为RTP流提供播放,拍摄,和数据转换机置。
Figure 9-1: RTP reception data flow.
一个单独的播放器通过session管理器被用于每个接收到的流。你可以通过标准Manager createPlayer机置为一个RTP流创建一个Player。你也可以:
l 使用一个具有RTP session参数的MediaLocator,并且通过调用Manager.createPlayer(MediaLocator)创建一个Player。
l 通过从流中找回的数据源为一个特殊的ReceiveStream创建一个Player,然后把它传入Manager.createPlayer(DataSource)。
如果你使用一个MediaLocator创建一个Player,你可以只播放第一个在session中被发现的RTP流。如果你想在session中回放多个RTP流,你需要直接使用SessionManager并为每个ReceiveStream创建一个Player。
为一个RTP session创建一个Player
你使用一个MediaLocator为一个RTP session创建一个Player时,Manager为第一次在session中被发现的流创建了一个Player。当数据已经被探测到时,Player提交一个RealizeCompleteEvent事件。
通过监听RealizeCompleteEvent,你可以确定是否有数据到达,并且Player是否能够播放任何数据。一旦Player提交这个事件,你可以重新找回它的可视和控制组件。
注意:因为一个播放RTP媒体流的Player在session探测到数据以前不能准确完成,你就不应该试着用Manager.createRealizedPlayer为一个RTP媒体流创建一个Player。在数据到达时Player才会返回,然后如果没有数据被探测到,试着去创建一个Realized Player将发生不确定的阻塞。
一个Player能输出一个RTP-speciifc控制,提供全部会议统计表和能通过SessionManager被用于注册动态有效载荷的RTPControl。
Example 9-1: Creating a Player for an RTP session (1 of 2)
|
String url= "rtp://224.144.251.104:49150/audio/1";
MediaLocator mrl= new MediaLocator(url);
if (mrl == null) {
System.err.println("Can't build MRL for RTP");
return false;
}
// Create a player for this rtp session
try {
player = Manager.createPlayer(mrl);
} catch (NoPlayerException e) {
System.err.println("Error:" + e);
return false;
} catch (MalformedURLException e) {
System.err.println("Error:" + e);
return false;
} catch (IOException e) {
System.err.println("Error:" + e);
return false;
}
if (player != null) {
if (this.player == null) {
|
this.player = player;
player.addControllerListener(this);
player.realize();
}
}
|
监听格式改变
当一个Player提交一个FormatChangeEvent事件时,它可能显示发生了一个有效载荷的改变。 用一个MediaLocator自动创建的Players处理有效载荷的改变。最多的情况,这个过程包含创建一个新的Player去控制一个新的格式。播放RTP媒体流的应用程序需要为FormatChangeEvents监听,以便他们在一个新的Player被创建时可以回应。
当一个FormatChangeEvent被提交时,检查这个Player对象是否被控制并且可视组件是否改变。如果是的话,一个新的Player已经被创建,你需要移除在旧Player对象组件上的引用,然后得到新Player对象的组件。
Example 9-2: Listening for RTP format changes (1 of 2)
|
public synchronized void controllerUpdate(ControllerEvent ce) {
if (ce instanceof FormatChangeEvent) {
Dimension vSize = new Dimension(320,0);
Component oldVisualComp = visualComp;
if ((visualComp = player.getVisualComponent()) != null) {
if (oldVisualComp != visualComp) {
if (oldVisualComp != null) {
oldVisualComp.remove(zoomMenu);
}
framePanel.remove(oldVisualComp);
vSize = visualComp.getPreferredSize();
vSize.width = (int)(vSize.width * defaultScale);
vSize.height = (int)(vSize.height * defaultScale);
framePanel.add(visualComp);
visualComp.setBounds(0,
0,
vSize.width,
vSize.height);
addPopupMenu(visualComp);
}
|
}
Component oldComp = controlComp;
controlComp = player.getControlPanelComponent();
if (controlComp != null)
{
if (oldComp != controlComp)
{
framePanel.remove(oldComp);
framePanel.add(controlComp);
if (controlComp != null) {
int prefHeight = controlComp
.getPreferredSize()
.height;
controlComp.setBounds(0,
vSize.height,
vSize.width,
prefHeight);
}
}
}
}
}
|
为每个新接收流创建一个RTP Player
要在一个session中播放所有的ReceiveStreams,你需要为每个流创建一个单独的Player。当一个新流被创建时,会议管理器提交一个NewReceiveStreamEvent事件。通常,你注册为一个ReceiveStreamListener,然后为每个新的ReceiveStream创建一个Player。要创建一个Player,你从ReceiveStream中找回数据源,再把它传入Manager.createPlayer。
要在一个session中为每个新接收流创建一个Player:
1.架起一个RTP session:
a. 创建一个SessionManager。例如,创建com.sun.media.rtp.RTPSessionMgr的实例。(RTPSessionMgr是一个用JMF引用执行提供的SessionManager的实现)
b. 调用RTPSessionMgs addReceiveStreamListener注册为一个监听器。
c. 通过调用RTPSessionMgr initSession初始化RTP.
d. 通过调用RTPSessionMgr startSession启动RTP session。
Example 9-3: Setting up an RTP session (1 of 2)
|
public SessionManager createManager(String address,
int port,
int ttl,
boolean listener,
boolean sendlistener)
{
mgr = (SessionManager)new com.sun.media.rtp.RTPSessionMgr();
if (mgr == null) return null;
mgr.addFormat(new AudioFormat(AudioFormat.DVI_RTP,
44100,
4,
1),
18);
if (listener) mgr.addReceiveStreamListener(this);
if (sendlistener) new RTPSendStreamWindow(mgr);
// ask session mgr to generate the local participant's CNAME
String cname = mgr.generateCNAME();
String username = null;
try {
username = System.getProperty("user.name");
} catch (SecurityException e){
username = "jmf-user";
}
// create our local Session Address
SessionAddress localaddr = new SessionAddress();
try{
InetAddress destaddr = InetAddress.getByName(address);
SessionAddress sessaddr = new SessionAddress(destaddr,
port,
destaddr,
port + 1);
SourceDescription[] userdesclist= new SourceDescription[]
{
new SourceDescription(SourceDescription
.SOURCE_DESC_EMAIL,
"jmf-user@sun.com",
|
1,
false),
new SourceDescription(SourceDescription
.SOURCE_DESC_CNAME,
cname,
1,
false),
new SourceDescription(SourceDescription
.SOURCE_DESC_TOOL,
"JMF RTP Player v2.0",
1,
false)
};
mgr.initSession(localaddr,
userdesclist,
0.05,
0.25);
mgr.startSession(sessaddr,ttl,null);
} catch (Exception e) {
System.err.println(e.getMessage());
return null;
}
return mgr;
}
|
2.在你的ReceiveStreamListener update方法中,注意NewReceiveStreamEvent事件,它显示了一个新的被探测到的数据流。
3.当一个NewReceiveStreamEvnet被发现,通过调用getReceiveStream从NewReceiveStreamEvnet中重新得到ReceiveStream。
4.通过调用getDataSource从ReceiveStream中重新得到RTP数据源。这是一个带着一个RTP-specific格式的PushBufferDataSource。例如,编码为一个DVI音频的播放器将会是DVI_RTP。
5.传递DataSource到Manager.createPlayer创建一个Player。因为Player将被成功的创建,所以为解码和解包RTP格式的数据有效的插件是必须的。
Example 9-4: Listening for NewReceiveStreamEvents
|
public void update( ReceiveStreamEvent event)
{
Player newplayer = null;
RTPPlayerWindow playerWindow = null;
// find the sourceRTPSM for this event
SessionManager source = (SessionManager)event.getSource();
// create a new player if a new recvstream is detected
if (event instanceof NewReceiveStreamEvent)
{
String cname = "Java Media Player";
ReceiveStream stream = null;
try
{
// get a handle over the ReceiveStream
stream =((NewReceiveStreamEvent)event)
.getReceiveStream();
Participant part = stream.getParticipant();
if (part != null) cname = part.getCNAME();
// get a handle over the ReceiveStream datasource
DataSource dsource = stream.getDataSource();
// create a player by passing datasource to the
// Media Manager
newplayer = Manager.createPlayer(dsource);
System.out.println("created player " + newplayer);
} catch (Exception e) {
System.err.println("NewReceiveStreamEvent exception "
+ e.getMessage());
return;
}
if (newplayer == null) return;
playerlist.addElement(newplayer);
newplayer.addControllerListener(this);
// send this player to player GUI
playerWindow = new RTPPlayerWindow( newplayer, cname);
}
}
|
See RTPUtil in RTPUtil for a complete example.
处理RTP有效载荷的改变
如果RTP Session中流的有效载荷改变了,ReceiveStream将提交一个RemotePayloadChangeEvent事件。一般说来,当有效载荷改变时,现有的Player将不能处理新的格式,而且当你试图表现新的有效载荷时,JMF会抛出一个异常。要避免这种情况,你的ReceiveStreamListener需要观察RemotePayloadChangeEvents事件。当一个RemotePayloadChangeEvent被侦测到时,你需要:
1.关闭现有的Player。
2.移除已删除的Player上的所有监听。
3.用同一个RTP数据源创建一个新的Player。
4.为新的Player得到可视和控制组件。
5.为新的Player添加必要的监听。
Example 9-5: Handling RTP payload changes (1 of 2)
|
public void update(ReceiveStreamEvent event) {
if (event instanceof RemotePayloadChangeEvent) {
// payload has changed. we need to close the old player
// and create a new player
if (newplayer != null) {
// stop player and wait for stop event
newplayer.stop();
// block until StopEvent received...
// remove controllerlistener
newplayer.removeControllerListener(listener);
// remove any visual and control components
// attached to this application
// close the player and wait for close event
newplayer.close();
// block until ControllerClosedEvent received...
try {
// when the player was closed, its datasource was
// disconnected. Now we must reconnect the data-
// source before a player can be created for it.
|
// This is the same datasource received from
// NewReceiveStreamEvent and used to create the
// initial rtp player
rtpsource.connect();
newplayer = Manager.createPlayer(rtpsource);
if (newplayer == null) {
System.err.println("Could not create player");
return;
}
newplayer.addControllerListener(listener);
newplayer.realize();
// when the new player is realized, retrieve its
// visual and control components
} catch (Exception e) {
System.err.println("could not create player");
}
}
}
}
|
控制输入RTP流的缓冲
通过SessionManager,你可以控制RTP接收端穿过输出的BufferControl。你能够用这个控制设置两个参数,缓冲长度和开端。
缓冲长度是接收器维护的缓冲区的大小。开端是数据总量的最小值,控制压入数据之前,它将被缓冲,或者允许数据被压入(jitter buffer)。当这个开端的最小值传入时,对象中的数据将唯一变得有效。如果被缓冲的数据数量少于开端,数据将再次被缓冲直到开端被传入。
缓冲的长度和开端值被指定为毫秒。缓冲的音频包的数量或者视频帧依靠输入流的格式。每个接收流包含它自己的默认值和所有缓冲长度的最大值和最小开端。(这个默认值和最大缓冲长度被依赖的执行。)
要得到一个session的BufferControl,你调用SessionManager上的getControl。你可以通过调用getControlComponent为BufferControl重新得到一个GUI 组件。
用RTPSocket 播放RTP流
RTP不依赖于传输协议。通用使用RTPSocket,你可以在任何一种网络上流化RTP。RTP socket的格式被设计成既有数据也有控制通道。每一个通道都有一个输入和输出流来流化数据在网络上的进出。
SessionManager期望从RTPSocket中接收单独的RTP包。用户有责任流化RTP包到RTPSocket。
要从RTPSocket中播放一个RTP流,传递这个socket到Manager.createPlayer创建Player.二选一的,你可以通过调用createPlayer(MediaLocator)创建一个Player,然后用一个RTP变量的新协议”rtpraw”传递一个MediaLocator。例如:
Manager.createPlayer( new MediaLocator(“rtpraw:// ”));
根据JMF播放器的创建机置,Manager将试图创建数据源定义在:
<protocol package-prefix>.media.protocol.rtpraw.DataSource
这必须是RTPSocket。RTPSocket的内容将被设置到rtpraw。Manager随后将试图创建一个<content-prefix>.media.content.rptraw.Handler类型的player,然后在其上设置RTPSocket。
注意:在<protocol package-prefix>.media.protocol.rtpraw.DataSource上创建的RTPSocket是你自己的RTPSocket的实现。JMF API没有定义一个默认的RTPSocket 的实现。这个RTPSocket的实现依赖于你所使用的底层传输协议。你的RTPSocket类必须位于<protocol package-prefix>.media.protocol.rtpraw.DataSource,并且它的控制和数据管道流必须被设置成接下来的例子的样子。
作为RTPSocket的RTPControl接口将被用于添加动态的有效载荷信息到RTP会议管理器。
接下来的例子实现了在UDP player上接收RTP UDP包和流化他们到Player或session管理器,而不被底层的网络/传输协议知道。这个例子使用定义在javax.media.rtp.RTPSocket中的接口和相关类。
Example 9-6: RTPSocketPlayer (1 of 6)
|
import java.io.*;
import java.net.*;
import java.util.*;
import javax.media.*;
import javax.media.format.*;
import javax.media.protocol.*;
import javax.media.rtp.*;
import javax.media.rtp.event.*;
import javax.media.rtp.rtcp.*;
public class RTPSocketPlayer implements ControllerListener {
// ENTER THE FOLLOWING SESSION PARAMETERS FOR YOUR RTP SESSION
// RTP Session address, multicast, unicast or broadcast address
String address = "224.144.251.245";
// RTP Session port
int port = 49150;
// Media Type i.e. one of audio or video
String media = "audio";
// DO NOT MODIFY ANYTHING BELOW THIS LINE
// The main rtpsocket abstraction which we will create and send
// to the Manager for appropriate handler creation
RTPSocket rtpsocket = null;
// The control RTPPushDataSource of the above RTPSocket
RTPPushDataSource rtcpsource = null;
// The GUI to handle the player
// PlayerWindow playerWindow;
// The handler created for the RTP session,
// as returned by the Manager
Player player;
// maximum size of buffer for UDP receive from the sockets
private int maxsize = 2000;
UDPHandler rtp = null;
UDPHandler rtcp = null;
public RTPSocketPlayer() {
// create the RTPSocket
rtpsocket = new RTPSocket();
|
// set its content type :
// rtpraw/video for a video session
// rtpraw/audio for an audio session
String content = "rtpraw/" + media;
rtpsocket.setContentType(content);
// set the RTP Session address and port of the RTP data
rtp = new UDPHandler(address, port);
// set the above UDP Handler to be the
// sourcestream of the rtpsocket
rtpsocket.setOutputStream(rtp);
// set the RTP Session address and port of the RTCP data
rtcp = new UDPHandler(address, port +1);
// get a handle over the RTCP Datasource so that we can
// set the sourcestream and deststream of this source
// to the rtcp udp handler we created above.
rtcpsource = rtpsocket.getControlChannel();
// Since we intend to send RTCP packets from the
// network to the session manager and vice-versa, we need
// to set the RTCP UDP handler as both the input and output
// stream of the rtcpsource.
rtcpsource.setOutputStream(rtcp);
rtcpsource.setInputStream(rtcp);
// connect the RTP socket data source before
// creating the player
try {
rtpsocket.connect();
player = Manager.createPlayer(rtpsocket);
rtpsocket.start();
} catch (NoPlayerException e) {
System.err.println(e.getMessage());
e.printStackTrace();
return;
}
catch (IOException e) {
System.err.println(e.getMessage());
e.printStackTrace();
return;
}
if (player != null) {
player.addControllerListener(this);
// send this player to out playerwindow
// playerWindow = new PlayerWindow(player);
}
}
|
public synchronized void controllerUpdate(ControllerEvent ce) {
if ((ce instanceof DeallocateEvent) ||
(ce instanceof ControllerErrorEvent)) {
// stop udp handlers
if (rtp != null) rtp.close();
if (rtcp != null) rtcp.close();
}
}
// method used by inner class UDPHandler to open a datagram or
// multicast socket as the case maybe
private DatagramSocket InitSocket(String sockaddress,
int sockport)
{
InetAddress addr = null;
DatagramSocket sock = null;
try {
addr = InetAddress.getByName(sockaddress);
if (addr.isMulticastAddress()) {
MulticastSocket msock;
msock = new MulticastSocket(sockport);
msock.joinGroup(addr);
sock = (DatagramSocket)msock;
}
else {
sock = new DatagramSocket(sockport,addr);
}
return sock;
}
catch (SocketException e) {
e.printStackTrace();
return null;
}
catch (UnknownHostException e) {
e.printStackTrace();
return null;
}
catch (IOException e) {
e.printStackTrace();
return null;
}
}
|
// INNER CLASS UDP Handler which will receive UDP RTP Packets and
// stream them to the handler of the sources stream. IN case of
// RTCP, it will also accept RTCP packets and send them on the
// underlying network.
public class UDPHandler extends Thread implements PushSourceStream,
OutputDataStream
{
DatagramSocket mysock;
DatagramPacket dp;
SourceTransferHandler outputHandler;
String myAddress;
int myport;
boolean closed = false;
// in the constructor we open the socket and create the main
// UDPHandler thread.
public UDPHandler(String haddress, int hport) {
myAddress = haddress;
myport = hport;
mysock = InitSocket(myAddress,myport);
setDaemon(true);
start();
}
// the main thread receives RTP data packets from the
// network and transfer's this data to the output handler of
// this stream.
public void run() {
int len;
while(true) {
if (closed) {
cleanup();
return;
}
try {
do {
dp = new DatagramPacket( new byte[maxsize],
maxsize);
mysock.receive(dp);
if (closed){
cleanup();
return;
}
len = dp.getLength();
|
if (len > (maxsize >> 1)) maxsize = len << 1;
}
while (len >= dp.getData().length);
}catch (Exception e){
cleanup();
return;
}
if (outputHandler != null) {
outputHandler.transferData(this);
}
}
}
public void close() {
closed = true;
}
private void cleanup() {
mysock.close();
stop();
}
// methods of PushSourceStream
public Object[] getControls() {
return new Object[0];
}
public Object getControl(String controlName) {
return null;
}
public ContentDescriptor getContentDescriptor() {
return null;
}
public long getContentLength() {
return SourceStream.LENGTH_UNKNOWN;
}
public boolean endOfStream() {
return false;
}
// method by which data is transferred from the underlying
// network to the session manager.
public int read(byte buffer[],
int offset,
int length)
{
|
System.arraycopy(dp.getData(),
0,
buffer,
offset,
dp.getLength());
return dp.getData().length;
}
public int getMinimumTransferSize(){
return dp.getLength();
}
public void setTransferHandler(SourceTransferHandler
transferHandler)
{
this.outputHandler = transferHandler;
}
// methods of OutputDataStream used by the session manager to
// transfer data to the underlying network.
public int write(byte[] buffer,
int offset,
int length)
{
InetAddress addr = null;
try {
addr = InetAddress.getByName(myAddress);
} catch (UnknownHostException e) {
e.printStackTrace();
}
DatagramPacket dp = new DatagramPacket( buffer,
length,
addr,
myport);
try {
mysock.send(dp);
} catch (IOException e){
e.printStackTrace();
}
return dp.getLength();
}
}
public static void main(String[] args) {
new RTPSocketPlayer();
}
}
|