Changed Video app to resize streams
git-svn-id: http://bigbluebutton.googlecode.com/svn/trunk@2394 af16638f-c34d-0410-8cfa-b39d5352b314
This commit is contained in:
parent
2b82ed313e
commit
00778a2e41
@ -1,13 +0,0 @@
|
||||
package org.bigbluebutton.app.video;
|
||||
|
||||
import org.red5.server.api.IScope;
|
||||
|
||||
import xugglerutils.JoinedStream;
|
||||
|
||||
public class JoinedStreamFactory {
|
||||
|
||||
public JoinedStream createStream(String room, IScope scope){
|
||||
return new JoinedStream(scope, room, VideoAppConstants.JOINED_WIDTH,
|
||||
VideoAppConstants.JOINED_HEIGHT, VideoAppConstants.JOINED_FRAME_RATE);
|
||||
}
|
||||
}
|
@ -11,4 +11,6 @@ public class VideoAppConstants {
|
||||
|
||||
public static final int TILE_WIDTH = 160;
|
||||
public static final int TILE_HEIGHT = 160;
|
||||
|
||||
public static final String TILE_PREFIX = "small";
|
||||
}
|
||||
|
@ -1,34 +1,27 @@
|
||||
package org.bigbluebutton.app.video;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.adapter.MultiThreadedApplicationAdapter;
|
||||
import org.red5.server.api.IBandwidthConfigure;
|
||||
import org.red5.server.api.IConnection;
|
||||
import org.red5.server.api.IScope;
|
||||
import org.red5.server.api.Red5;
|
||||
import org.red5.server.api.stream.IBroadcastStream;
|
||||
import org.red5.server.api.stream.IServerStream;
|
||||
import org.red5.server.api.stream.IStreamCapableConnection;
|
||||
import org.red5.server.api.stream.support.SimpleConnectionBWConfig;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import xugglerutils.JoinedStream;
|
||||
import xugglerutils.StreamDownsizer;
|
||||
|
||||
public class VideoApplication extends MultiThreadedApplicationAdapter {
|
||||
private static Logger log = Red5LoggerFactory.getLogger(VideoApplication.class, "video");
|
||||
|
||||
private VideoTranscoder videoTranscoder = new VideoTranscoder();
|
||||
|
||||
private IScope appScope;
|
||||
|
||||
private IServerStream serverStream;
|
||||
private ArrayList<JoinedStream> joinedStreams;
|
||||
|
||||
@Override
|
||||
public boolean appStart(IScope app) {
|
||||
super.appStart(app);
|
||||
log.info("video appStart");
|
||||
joinedStreams = new ArrayList<JoinedStream>();
|
||||
appScope = app;
|
||||
return true;
|
||||
}
|
||||
@ -54,30 +47,28 @@ public class VideoApplication extends MultiThreadedApplicationAdapter {
|
||||
@Override
|
||||
public void appDisconnect(IConnection conn) {
|
||||
log.info("oflaDemo appDisconnect");
|
||||
if (appScope == conn.getScope() && serverStream != null) {
|
||||
serverStream.close();
|
||||
}
|
||||
super.appDisconnect(conn);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called on publish: NetStream.publish("streamname", "live")
|
||||
*/
|
||||
@Override
|
||||
public void streamPublishStart(IBroadcastStream stream){
|
||||
super.streamPublishStart(stream);
|
||||
|
||||
StreamDownsizer smallStream = new StreamDownsizer(stream);
|
||||
public void streamPublishStart(IBroadcastStream stream) {
|
||||
log.debug("streamPublishStart: {}; {}", stream, stream.getPublishedName());
|
||||
super.streamPublishStart(stream);
|
||||
videoTranscoder.startTranscodingStream(stream,
|
||||
Red5.getConnectionLocal().getScope());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean roomStart(IScope room){
|
||||
boolean ret = super.roomStart(room);
|
||||
|
||||
JoinedStream videoStream = new JoinedStream(room, room.getName(),
|
||||
VideoAppConstants.JOINED_WIDTH, VideoAppConstants.JOINED_HEIGHT, VideoAppConstants.JOINED_FRAME_RATE);
|
||||
joinedStreams.add(videoStream);
|
||||
Thread videoThread = new Thread(videoStream, room.getName());
|
||||
videoThread.start();
|
||||
|
||||
return ret;
|
||||
public void streamBroadcastClose(IBroadcastStream stream) {
|
||||
log.debug("streamBroadcastClose: {}; {}", stream, stream.getPublishedName());
|
||||
|
||||
videoTranscoder.stopTranscodingStream(stream,
|
||||
Red5.getConnectionLocal().getScope());
|
||||
super.streamBroadcastClose(stream);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,152 @@
|
||||
package org.bigbluebutton.app.video;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.api.IContext;
|
||||
import org.red5.server.api.IScope;
|
||||
import org.red5.server.api.stream.IBroadcastStream;
|
||||
import org.red5.server.stream.BroadcastScope;
|
||||
import org.red5.server.stream.IBroadcastScope;
|
||||
import org.red5.server.stream.IProviderService;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.xuggle.red5.IVideoPictureListener;
|
||||
import com.xuggle.red5.Transcoder;
|
||||
import com.xuggle.red5.VideoPictureListener;
|
||||
import com.xuggle.red5.io.BroadcastStream;
|
||||
import com.xuggle.xuggler.ICodec;
|
||||
import com.xuggle.xuggler.ISimpleMediaFile;
|
||||
import com.xuggle.xuggler.IVideoPicture;
|
||||
import com.xuggle.xuggler.IVideoResampler;
|
||||
import com.xuggle.xuggler.SimpleMediaFile;
|
||||
|
||||
public class VideoTranscoder {
|
||||
|
||||
final private Logger log = Red5LoggerFactory.getLogger(this.getClass());
|
||||
|
||||
final private Map<String, BroadcastStream> mOutputStreams = new HashMap<String, BroadcastStream>();
|
||||
final private Map<String, Transcoder> mTranscoders = new HashMap<String, Transcoder>();
|
||||
|
||||
|
||||
final private IVideoPictureListener pictureListener = new VideoPictureListener(){
|
||||
|
||||
public IVideoPicture preEncode(IVideoPicture picture){
|
||||
IVideoPicture outPicture = IVideoPicture.make(picture.getPixelType(),
|
||||
VideoAppConstants.TILE_WIDTH, VideoAppConstants.TILE_HEIGHT);
|
||||
|
||||
IVideoResampler resampler = IVideoResampler.make(VideoAppConstants.TILE_WIDTH,
|
||||
VideoAppConstants.TILE_HEIGHT, picture.getPixelType(),
|
||||
picture.getWidth(), picture.getHeight(), picture.getPixelType());
|
||||
|
||||
resampler.resample(outPicture, picture);
|
||||
|
||||
return outPicture;
|
||||
}
|
||||
};
|
||||
|
||||
public VideoTranscoder(){
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts transcoding this stream. This method is a no-op if
|
||||
* this stream is already a stream copy created by this
|
||||
* transcoder.
|
||||
* @param aStream The stream to copy.
|
||||
* @param aScope The application scope.
|
||||
*/
|
||||
synchronized public void startTranscodingStream(IBroadcastStream aStream, IScope aScope)
|
||||
{
|
||||
log.debug("startTranscodingStream({},{})", aStream.getPublishedName(), aScope.getName());
|
||||
if (aStream.getPublishedName().startsWith(VideoAppConstants.TILE_PREFIX))
|
||||
{
|
||||
log.debug("Not making a copy of a copy: {}", aStream.getPublishedName());
|
||||
return;
|
||||
}
|
||||
log.debug("Making transcoded version of: {}", aStream.getPublishedName());
|
||||
|
||||
/*
|
||||
* Now, we need to set up the output stream we want to broadcast to.
|
||||
* Turns out aaffmpeg-red5 provides one of those.
|
||||
*/
|
||||
String outputName = VideoAppConstants.TILE_PREFIX+aStream.getPublishedName();
|
||||
BroadcastStream outputStream = new BroadcastStream(outputName);
|
||||
outputStream.setPublishedName(outputName);
|
||||
outputStream.setScope(aScope);
|
||||
|
||||
IContext context = aScope.getContext();
|
||||
|
||||
IProviderService providerService = (IProviderService) context
|
||||
.getBean(IProviderService.BEAN_NAME);
|
||||
if (providerService.registerBroadcastStream(aScope, outputName,
|
||||
outputStream))
|
||||
{
|
||||
IBroadcastScope bsScope = (BroadcastScope) providerService
|
||||
.getLiveProviderInput(aScope, outputName, true);
|
||||
|
||||
|
||||
bsScope.setAttribute(IBroadcastScope.STREAM_ATTRIBUTE, outputStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
log.error("Got a fatal error; could not register broadcast stream");
|
||||
throw new RuntimeException("fooey!");
|
||||
}
|
||||
mOutputStreams.put(aStream.getPublishedName(), outputStream);
|
||||
outputStream.start();
|
||||
|
||||
/**
|
||||
* Now let's give aaffmpeg-red5 some information about what we want to transcode as.
|
||||
*/
|
||||
ISimpleMediaFile outputStreamInfo = new SimpleMediaFile();
|
||||
outputStreamInfo.setHasAudio(true);
|
||||
outputStreamInfo.setAudioBitRate(32000);
|
||||
outputStreamInfo.setAudioChannels(1);
|
||||
outputStreamInfo.setAudioSampleRate(22050);
|
||||
outputStreamInfo.setAudioCodec(ICodec.ID.CODEC_ID_MP3);
|
||||
outputStreamInfo.setHasVideo(true);
|
||||
// Unfortunately the Trans-coder needs to know the width and height
|
||||
// you want to output as; even if you don't know yet.
|
||||
outputStreamInfo.setVideoWidth(320);
|
||||
outputStreamInfo.setVideoHeight(240);
|
||||
outputStreamInfo.setVideoBitRate(320000);
|
||||
outputStreamInfo.setVideoCodec(ICodec.ID.CODEC_ID_FLV1);
|
||||
outputStreamInfo.setVideoGlobalQuality(0);
|
||||
|
||||
/**
|
||||
* And finally, let's create out transcoder
|
||||
*/
|
||||
Transcoder transcoder = new Transcoder(aStream,
|
||||
outputStream, outputStreamInfo,
|
||||
null, null, pictureListener);
|
||||
Thread transcoderThread = new Thread(transcoder);
|
||||
transcoderThread.setDaemon(true);
|
||||
mTranscoders.put(aStream.getPublishedName(), transcoder);
|
||||
log.debug("Starting transcoding thread for: {}", aStream.getPublishedName());
|
||||
transcoderThread.start();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop transcoding a stream.
|
||||
* @param aStream The stream to stop transcoding.
|
||||
* @param aScope The application scope.
|
||||
*/
|
||||
synchronized public void stopTranscodingStream(IBroadcastStream aStream, IScope aScope)
|
||||
{
|
||||
log.debug("stopTranscodingStream({},{})", aStream.getPublishedName(), aScope.getName());
|
||||
String inputName = aStream.getPublishedName();
|
||||
Transcoder transcoder = mTranscoders.get(inputName);
|
||||
if (transcoder != null)
|
||||
{
|
||||
transcoder.stop();
|
||||
}
|
||||
BroadcastStream outputStream = mOutputStreams.get(inputName);
|
||||
if (outputStream != null)
|
||||
{
|
||||
outputStream.stop();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package xugglerutils;
|
||||
|
||||
import java.awt.Graphics2D;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import org.bigbluebutton.app.video.VideoAppConstants;
|
||||
|
||||
public class CheckerboardProcessor {
|
||||
|
||||
private ArrayList<IImageProvider> imageProviders;
|
||||
|
||||
private BufferedImage image;
|
||||
private Graphics2D graphics;
|
||||
|
||||
public CheckerboardProcessor(){
|
||||
imageProviders = new ArrayList<IImageProvider>();
|
||||
|
||||
this.image = new BufferedImage(VideoAppConstants.JOINED_WIDTH,
|
||||
VideoAppConstants.JOINED_HEIGHT, BufferedImage.TYPE_3BYTE_BGR);
|
||||
this.graphics = this.image.createGraphics();
|
||||
}
|
||||
|
||||
public BufferedImage getImage(){
|
||||
return this.image;
|
||||
}
|
||||
|
||||
public void registerListener(IImageProvider provider){
|
||||
this.imageProviders.add(provider);
|
||||
}
|
||||
|
||||
public void updateImage(){
|
||||
for (int i = 0; i<imageProviders.size(); i++){
|
||||
appendImage(imageProviders.get(i).getImage(), i);
|
||||
}
|
||||
}
|
||||
|
||||
public void appendImage(BufferedImage image, int position){
|
||||
int x = 0;
|
||||
int y = 0;
|
||||
if (position == 0){
|
||||
x=0;
|
||||
y=0;
|
||||
} else if (position == 1){
|
||||
x=VideoAppConstants.TILE_WIDTH;
|
||||
y=0;
|
||||
} else if (position == 2){
|
||||
x=VideoAppConstants.TILE_WIDTH*2;
|
||||
y=0;
|
||||
} else if (position == 3){
|
||||
x=0;
|
||||
y=VideoAppConstants.TILE_HEIGHT;
|
||||
} else if (position == 4){
|
||||
x=VideoAppConstants.TILE_WIDTH;
|
||||
y=VideoAppConstants.TILE_HEIGHT;
|
||||
} else if (position == 5){
|
||||
x=VideoAppConstants.TILE_WIDTH*2;
|
||||
y=VideoAppConstants.TILE_HEIGHT;
|
||||
} else if (position == 6){
|
||||
x=0;
|
||||
y=VideoAppConstants.TILE_HEIGHT*2;
|
||||
} else if (position == 7){
|
||||
x=VideoAppConstants.TILE_WIDTH;
|
||||
y=VideoAppConstants.TILE_HEIGHT*2;
|
||||
} else if (position == 8){
|
||||
y=VideoAppConstants.TILE_HEIGHT*2;
|
||||
x=VideoAppConstants.TILE_WIDTH*2;
|
||||
}
|
||||
graphics.drawImage(image, x, y, null);
|
||||
}
|
||||
}
|
@ -1,7 +0,0 @@
|
||||
package xugglerutils;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
public interface IImageProvider {
|
||||
public BufferedImage getImage();
|
||||
}
|
@ -1,214 +0,0 @@
|
||||
package xugglerutils;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.bigbluebutton.app.video.VideoAppConstants;
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.api.IContext;
|
||||
import org.red5.server.api.IScope;
|
||||
import org.red5.server.net.rtmp.event.IRTMPEvent;
|
||||
import org.red5.server.stream.BroadcastScope;
|
||||
import org.red5.server.stream.IBroadcastScope;
|
||||
import org.red5.server.stream.IProviderService;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.xuggle.red5.io.BroadcastStream;
|
||||
import com.xuggle.red5.io.IRTMPEventIOHandler;
|
||||
import com.xuggle.red5.io.Red5HandlerFactory;
|
||||
import com.xuggle.red5.io.Red5Message;
|
||||
import com.xuggle.xuggler.ICodec;
|
||||
import com.xuggle.xuggler.IContainer;
|
||||
import com.xuggle.xuggler.IContainerFormat;
|
||||
import com.xuggle.xuggler.IPacket;
|
||||
import com.xuggle.xuggler.IPixelFormat;
|
||||
import com.xuggle.xuggler.IRational;
|
||||
import com.xuggle.xuggler.ISimpleMediaFile;
|
||||
import com.xuggle.xuggler.IStream;
|
||||
import com.xuggle.xuggler.IStreamCoder;
|
||||
import com.xuggle.xuggler.IVideoPicture;
|
||||
import com.xuggle.xuggler.IVideoResampler;
|
||||
import com.xuggle.xuggler.SimpleMediaFile;
|
||||
import com.xuggle.xuggler.video.ConverterFactory;
|
||||
import com.xuggle.xuggler.video.IConverter;
|
||||
|
||||
public class JoinedStream implements Runnable {
|
||||
final private Logger log = Red5LoggerFactory.getLogger(JoinedStream.class, "deskshare");
|
||||
|
||||
private IStreamCoder outStreamCoder;
|
||||
private BroadcastStream broadcastStream;
|
||||
private IContainer outContainer;
|
||||
private IStream outStream;
|
||||
private CheckerboardProcessor imageProcessor;
|
||||
|
||||
private static final Red5HandlerFactory factory = Red5HandlerFactory.getFactory();
|
||||
private final IRTMPEventIOHandler outputHandler;
|
||||
private boolean keepStreaming = true;
|
||||
|
||||
private long timestamp = 0, frameNumber = 0;
|
||||
private int width, height, frameRate, timestampBase;
|
||||
private int encodingWidth, encodingHeight;
|
||||
private String outStreamName;
|
||||
private IScope scope;
|
||||
|
||||
public JoinedStream(IScope scope, String streamName, int width, int height, int frameRate){
|
||||
this.scope = scope;
|
||||
this.outStreamName = streamName;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.frameRate = frameRate;
|
||||
this.timestampBase = 1000000 / this.frameRate;
|
||||
this.imageProcessor = new CheckerboardProcessor();
|
||||
|
||||
outputHandler = new IRTMPEventIOHandler(){
|
||||
public Red5Message read() throws InterruptedException{
|
||||
return null;
|
||||
}
|
||||
|
||||
public void write(Red5Message message) throws InterruptedException{
|
||||
log.debug("Handler writing message to stream " + outStreamName);
|
||||
try{
|
||||
IRTMPEvent event = message.getData();
|
||||
if (event != null){
|
||||
broadcastStream.dispatchEvent(event);
|
||||
event.release();
|
||||
}
|
||||
} finally{
|
||||
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void run(){
|
||||
startPublishing(scope);
|
||||
while(keepStreaming){
|
||||
encodePicture(imageProcessor.getImage());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts outputting captured video to a red5 stream
|
||||
* @param aScope
|
||||
*/
|
||||
synchronized private void startPublishing(IScope aScope){
|
||||
log.debug("started publishing stream in " + aScope.getName());
|
||||
|
||||
broadcastStream = new BroadcastStream(outStreamName);
|
||||
broadcastStream.setPublishedName(outStreamName);
|
||||
broadcastStream.setScope(aScope);
|
||||
|
||||
IContext context = aScope.getContext();
|
||||
|
||||
IProviderService providerService = (IProviderService) context.getBean(IProviderService.BEAN_NAME);
|
||||
if (providerService.registerBroadcastStream(aScope, outStreamName, broadcastStream)){
|
||||
IBroadcastScope bScope = (BroadcastScope) providerService.getLiveProviderInput(aScope, outStreamName, true);
|
||||
|
||||
bScope.setAttribute(IBroadcastScope.STREAM_ATTRIBUTE, broadcastStream);
|
||||
} else{
|
||||
log.error("could not register broadcast stream");
|
||||
throw new RuntimeException("could not register broadcast stream");
|
||||
}
|
||||
|
||||
broadcastStream.start();
|
||||
}
|
||||
|
||||
private void encodePicture(BufferedImage screen) {
|
||||
IPacket packet = IPacket.make();
|
||||
|
||||
IConverter converter = null;
|
||||
IVideoResampler resampler = IVideoResampler.make(encodingWidth, encodingHeight, IPixelFormat.Type.YUV420P,
|
||||
width, height, IPixelFormat.Type.BGR24);
|
||||
try{
|
||||
converter = ConverterFactory.createConverter(screen, IPixelFormat.Type.BGR24);
|
||||
} catch(UnsupportedOperationException e){
|
||||
log.error("could not create converter");
|
||||
}
|
||||
|
||||
IVideoPicture inFrame = converter.toPicture(screen, timestamp);
|
||||
IVideoPicture outFrame = IVideoPicture.make(IPixelFormat.Type.YUV420P, encodingWidth, encodingHeight);
|
||||
resampler.resample(outFrame, inFrame);
|
||||
log.debug(outFrame.getPixelType().toString());
|
||||
timestamp += timestampBase;
|
||||
frameNumber ++;
|
||||
|
||||
outFrame.setQuality(0);
|
||||
int retval = outStreamCoder.encodeVideo(packet, outFrame, 0);
|
||||
if (retval < 0)
|
||||
throw new RuntimeException("could not encode video");
|
||||
if (packet.isComplete()){
|
||||
retval = outContainer.writePacket(packet, false);
|
||||
if (retval < 0)
|
||||
throw new RuntimeException("could not save packet to container");
|
||||
}
|
||||
outFrame.delete();
|
||||
outFrame = null;
|
||||
converter = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up Xuggler containers & streams
|
||||
*/
|
||||
private void setupStreams(){
|
||||
log.debug("Setting up streams: {}", broadcastStream.getName());
|
||||
String outputURL = Red5HandlerFactory.DEFAULT_PROTOCOL +":"+ broadcastStream.getName();
|
||||
|
||||
ICodec videoCodec = ICodec.findEncodingCodec(ICodec.ID.CODEC_ID_FLV1);
|
||||
|
||||
ISimpleMediaFile outInfo = new SimpleMediaFile();
|
||||
outInfo.setURL(outputURL);
|
||||
outInfo.setHasVideo(true);
|
||||
outInfo.setHasAudio(false);
|
||||
outInfo.setVideoWidth(encodingWidth);
|
||||
outInfo.setVideoHeight(encodingHeight);
|
||||
outInfo.setVideoBitRate(VideoAppConstants.BIT_RATE);
|
||||
outInfo.setVideoPixelFormat(IPixelFormat.Type.YUV420P);
|
||||
outInfo.setVideoNumPicturesInGroupOfPictures(VideoAppConstants.NUM_PICTURES_IN_GROUP);
|
||||
outInfo.setVideoCodec(ICodec.ID.CODEC_ID_FLV1);
|
||||
outInfo.setVideoGlobalQuality(0);
|
||||
|
||||
factory.registerStream(outputHandler, outInfo);
|
||||
|
||||
outContainer = IContainer.make();
|
||||
IContainerFormat outFormat = IContainerFormat.make();
|
||||
outFormat.setOutputFormat("flv", outputURL, null);
|
||||
int retval = outContainer.open(outputURL, IContainer.Type.WRITE, outFormat);
|
||||
if (retval <0){
|
||||
log.error("could not open output container");
|
||||
throw new RuntimeException("could not open output file");
|
||||
}
|
||||
log.debug("Output container is open.");
|
||||
outStream = outContainer.addNewStream(0);
|
||||
outStreamCoder = outStream.getStreamCoder();
|
||||
|
||||
outStreamCoder.setNumPicturesInGroupOfPictures(VideoAppConstants.NUM_PICTURES_IN_GROUP);
|
||||
outStreamCoder.setCodec(videoCodec);
|
||||
outStreamCoder.setBitRate(VideoAppConstants.BIT_RATE);
|
||||
outStreamCoder.setBitRateTolerance(VideoAppConstants.BIT_RATE_TOLERANCE);
|
||||
|
||||
outStreamCoder.setPixelType(IPixelFormat.Type.YUV420P);
|
||||
outStreamCoder.setHeight(encodingHeight);
|
||||
outStreamCoder.setWidth(encodingWidth);
|
||||
outStreamCoder.setFlag(IStreamCoder.Flags.FLAG_QSCALE, true);
|
||||
outStreamCoder.setGlobalQuality(0);
|
||||
|
||||
IRational frameRate = IRational.make(this.frameRate,1);
|
||||
outStreamCoder.setFrameRate(frameRate);
|
||||
IRational timeBase = IRational.make(frameRate.getDenominator(), frameRate.getNumerator());
|
||||
outStreamCoder.setTimeBase(timeBase);
|
||||
frameRate = null;
|
||||
|
||||
retval = outStreamCoder.open();
|
||||
if (retval <0)
|
||||
throw new RuntimeException("could not open input decoder");
|
||||
retval = outContainer.writeHeader();
|
||||
if (retval <0) {
|
||||
log.error("could not write file header");
|
||||
throw new RuntimeException("could not write file header");
|
||||
}
|
||||
}
|
||||
|
||||
public IScope getScope() {
|
||||
return scope;
|
||||
}
|
||||
}
|
@ -1,222 +0,0 @@
|
||||
package xugglerutils;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
|
||||
import org.apache.mina.core.buffer.IoBuffer;
|
||||
import org.bigbluebutton.app.video.VideoAppConstants;
|
||||
import org.red5.logging.Red5LoggerFactory;
|
||||
import org.red5.server.api.stream.IBroadcastStream;
|
||||
import org.red5.server.api.stream.IStreamListener;
|
||||
import org.red5.server.net.rtmp.event.IRTMPEvent;
|
||||
import org.red5.server.net.rtmp.event.VideoData;
|
||||
import org.red5.server.api.stream.IStreamPacket;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import com.xuggle.red5.IPacketListener;
|
||||
import com.xuggle.red5.IVideoPictureListener;
|
||||
import com.xuggle.red5.io.Red5HandlerFactory;
|
||||
import com.xuggle.red5.io.Red5Message;
|
||||
import com.xuggle.red5.io.Red5StreamingQueue;
|
||||
import com.xuggle.xuggler.IContainer;
|
||||
import com.xuggle.xuggler.IContainerFormat;
|
||||
import com.xuggle.xuggler.IPacket;
|
||||
import com.xuggle.xuggler.ISimpleMediaFile;
|
||||
import com.xuggle.xuggler.IStreamCoder;
|
||||
import com.xuggle.xuggler.IVideoPicture;
|
||||
import com.xuggle.xuggler.IVideoResampler;
|
||||
import com.xuggle.xuggler.SimpleMediaFile;
|
||||
|
||||
public class StreamDownsizer implements IImageProvider, Runnable {
|
||||
|
||||
final private Logger log = Red5LoggerFactory.getLogger(this.getClass());
|
||||
final static private Red5HandlerFactory red5Factory = Red5HandlerFactory.getFactory();
|
||||
|
||||
private IBroadcastStream inputStream;
|
||||
private IStreamListener inputListener;
|
||||
private Red5StreamingQueue inputQueue;
|
||||
|
||||
private String inputURL;
|
||||
private IContainer inContainer;
|
||||
private IStreamCoder inCoder;
|
||||
private IVideoResampler resampler;
|
||||
|
||||
private int videoStreamId;
|
||||
private boolean keepRunning = true;
|
||||
|
||||
public StreamDownsizer(IBroadcastStream inputStream){
|
||||
|
||||
this.inputStream = inputStream;
|
||||
this.inputQueue = new Red5StreamingQueue();
|
||||
|
||||
this.videoStreamId = -1;
|
||||
|
||||
inputListener = new IStreamListener(){
|
||||
public void packetReceived(IBroadcastStream aStream, IStreamPacket aPacket)
|
||||
{
|
||||
try {
|
||||
|
||||
|
||||
IoBuffer buf = aPacket.getData();
|
||||
if (buf != null)
|
||||
buf.rewind();
|
||||
if (buf==null || buf.remaining()==0)
|
||||
{
|
||||
log.debug("skipping empty packet with no data");
|
||||
return;
|
||||
}
|
||||
|
||||
if (aPacket instanceof VideoData)
|
||||
{
|
||||
Red5Message.Type type = Red5Message.Type.INTERFRAME;
|
||||
VideoData dataPacket = (VideoData)aPacket;
|
||||
switch(dataPacket.getFrameType())
|
||||
{
|
||||
case DISPOSABLE_INTERFRAME:
|
||||
type = Red5Message.Type.DISPOSABLE_INTERFRAME;
|
||||
break;
|
||||
case INTERFRAME:
|
||||
type = Red5Message.Type.INTERFRAME;
|
||||
break;
|
||||
case KEYFRAME:
|
||||
case UNKNOWN:
|
||||
type = Red5Message.Type.KEY_FRAME;
|
||||
break;
|
||||
}
|
||||
if (type != Red5Message.Type.DISPOSABLE_INTERFRAME) // The FFMPEG FLV decoder doesn't handle disposable frames
|
||||
{
|
||||
log.debug(" adding packet type: {}; ts: {}; on stream: {}",
|
||||
new Object[]{dataPacket.getFrameType(), aPacket.getTimestamp(), aStream.getPublishedName()});
|
||||
inputQueue.put(new Red5Message(type, dataPacket));
|
||||
}
|
||||
} else if (aPacket instanceof IRTMPEvent)
|
||||
{
|
||||
log.debug(" adding packet type: {}; ts: {}; on stream: {}",
|
||||
new Object[]{"OTHER", aPacket.getTimestamp(), aStream.getPublishedName()});
|
||||
Red5Message.Type type = Red5Message.Type.OTHER;
|
||||
IRTMPEvent dataPacket = (IRTMPEvent)aPacket;
|
||||
inputQueue.put(new Red5Message(type, dataPacket));
|
||||
}
|
||||
else
|
||||
{
|
||||
log.debug("dropping packet type: {}; ts: {}; on stream: {}",
|
||||
new Object[]{"UNKNOWN", aPacket.getTimestamp(), aStream.getPublishedName()});
|
||||
}
|
||||
} catch (InterruptedException ex)
|
||||
{
|
||||
log.error("exception: {}", ex);
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
private void openContainer()
|
||||
{
|
||||
try {
|
||||
// set out thread name
|
||||
String threadName = "Transcoder["+inputStream.getPublishedName()+"]";
|
||||
log.debug("Changing thread name: {}; to {};", Thread.currentThread().getName(), threadName);
|
||||
Thread.currentThread().setName(threadName);
|
||||
int retval = -1;
|
||||
|
||||
// First let's setup our input URL
|
||||
{
|
||||
// Register a new listener; should hopefully start getting audio packets immediately
|
||||
log.debug("Adding packet listener to stream: {}", inputStream.getPublishedName());
|
||||
inputStream.addStreamListener(inputListener);
|
||||
|
||||
// Tell AAFFMPEG about our new input URL; we use the unique Red5 names for the url
|
||||
inputURL = Red5HandlerFactory.DEFAULT_PROTOCOL+":"+inputStream.getName();
|
||||
ISimpleMediaFile inputInfo = new SimpleMediaFile();
|
||||
inputInfo.setURL(inputURL);
|
||||
red5Factory.registerStream(inputQueue, inputInfo);
|
||||
|
||||
inContainer = IContainer.make();
|
||||
// NOTE: This will block until we get the later of the first audio if it has audio, or first video
|
||||
// if it has video
|
||||
log.debug("About to open input url: {}", inputURL);
|
||||
IContainerFormat inFormat = IContainerFormat.make();
|
||||
inFormat.setInputFormat("flv"); // set the input format to avoid FFMPEG probing
|
||||
retval = inContainer.open(inputURL, IContainer.Type.READ, inFormat, true, false);
|
||||
if (retval < 0)
|
||||
{
|
||||
throw new RuntimeException("Could not open input: " + inputURL);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public BufferedImage getImage() {
|
||||
// TODO Auto-generated method stub
|
||||
return null;
|
||||
}
|
||||
|
||||
public void stop()
|
||||
{
|
||||
try
|
||||
{
|
||||
inputQueue.put(new Red5Message(Red5Message.Type.END_STREAM, null));
|
||||
}
|
||||
catch (InterruptedException e)
|
||||
{
|
||||
log.error("exception: {}", e);
|
||||
}
|
||||
keepRunning = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// TODO Auto-generated method stub
|
||||
try
|
||||
{
|
||||
openContainer();
|
||||
decode();
|
||||
}
|
||||
catch (Throwable e)
|
||||
{
|
||||
log.error("uncaught exception: " + e.getMessage());
|
||||
e.printStackTrace();
|
||||
}
|
||||
finally
|
||||
{
|
||||
//closeContainer();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void decode(){
|
||||
IPacket packet = IPacket.make();
|
||||
|
||||
while(keepRunning){
|
||||
int ret = inContainer.readNextPacket(packet);
|
||||
if (ret<0) {
|
||||
log.debug("Container empty, stopping stream");
|
||||
keepRunning = false;
|
||||
}
|
||||
IVideoPicture resizedVideoPicture = resizeVideoPicture(packet);
|
||||
}
|
||||
}
|
||||
|
||||
private IVideoPicture resizeVideoPicture(IPacket packet){
|
||||
IVideoPicture videoPicture = IVideoPicture.make(inCoder.getPixelType(),
|
||||
inCoder.getWidth(), inCoder.getHeight());
|
||||
IVideoPicture resampledPicture = IVideoPicture.make(inCoder.getPixelType(),
|
||||
VideoAppConstants.TILE_WIDTH, VideoAppConstants.TILE_HEIGHT);
|
||||
|
||||
IVideoResampler resampler = IVideoResampler.make(VideoAppConstants.TILE_WIDTH, VideoAppConstants.TILE_HEIGHT,
|
||||
inCoder.getPixelType(), inCoder.getWidth(), inCoder.getHeight(), inCoder.getPixelType());
|
||||
|
||||
resampler.resample(resampledPicture, videoPicture);
|
||||
|
||||
return videoPicture;
|
||||
}
|
||||
|
||||
|
||||
}
|
Loading…
Reference in New Issue
Block a user