Commit 8253e30e authored by Maik Merten's avatar Maik Merten
Browse files

Merge branch 'master' of git.xiph.org:cortado

parents d8611fc9 136405c2
......@@ -134,7 +134,11 @@ parameters:
Defaults to auto
duration: int
Length of clip in seconds. Needed when seekable is true,
Length of clip in seconds. Needed when seekable is false,
to allow the seek bar to work.
startTime: int
Start time of clip in seconds. Needed when seekable is false,
to allow the seek bar to work.
keepAspect: boolean
......
......@@ -12,7 +12,7 @@
public double currentTime;
public double duration;
public boolean paused;
public String src;
public java.lang.String src;
public void run();
public void doPause();
public void doPlay();
......
......@@ -26,7 +26,7 @@ import com.fluendo.utils.*;
public class Item {
private Tracker kin = null;
private boolean alive = false;
private boolean active = false;
private Font font = null;
private int font_size = 0;
private String text = null;
......@@ -116,7 +116,7 @@ public class Item {
/**
* Updates the item at the given time.
* returns true for alive, false for dead
* returns false if the item should be destroyed, true otherwise.
*/
public boolean update(Component c, Dimension d, double t) {
com.fluendo.jkate.Event ev = kin.ev;
......@@ -125,13 +125,13 @@ public class Item {
/* early out if we're not within the lifetime of the event */
if (t < ev.start_time) return true;
if (t >= ev.end_time) {
alive = false;
active = false;
dirty = true;
return false; /* we're done, and will get destroyed */
}
if (!alive) {
alive = true;
if (!active) {
active = true;
dirty = true;
}
......@@ -161,7 +161,7 @@ public class Item {
* Renders the item on the given image.
*/
public void render(Component c, Image img) {
if (!alive)
if (!active)
return;
updateCachedData(c, img);
......@@ -209,4 +209,7 @@ public class Item {
public boolean isDirty() {
return dirty;
}
public boolean isActive() {
return active;
}
}
......@@ -30,7 +30,7 @@ public class Renderer {
/**
* Add a new event to the renderer.
*/
public void add(com.fluendo.jkate.Event ev) {
public synchronized void add(com.fluendo.jkate.Event ev) {
items.addElement(new Item(ev));
dirty = true;
}
......@@ -39,21 +39,26 @@ public class Renderer {
* Update the renderer, and all the events it tracks.
* Returns 1 if there is nothing to draw, as an optimization
*/
public int update(Component c, Dimension d, double t) {
public synchronized int update(Component c, Dimension d, double t) {
int nactive = 0;
for (int n=0; n<items.size(); ++n) {
boolean ret = ((Item)items.elementAt(n)).update(c, d, t);
Item item = (Item)items.elementAt(n);
boolean ret = item.update(c, d, t);
if (!ret) {
items.removeElementAt(n);
dirty = true;
--n;
}
else {
if (((Item)items.elementAt(n)).isDirty()) {
if (item.isDirty()) {
dirty = true;
}
if (item.isActive()) {
++nactive;
}
}
}
if (items.size() == 0)
if (nactive == 0)
return 1;
return 0;
}
......@@ -61,7 +66,7 @@ public class Renderer {
/**
* Renders onto the given image.
*/
public Image render(Component c, Image img) {
public synchronized Image render(Component c, Image img) {
/* there used to be some non copying code using BufferedImage, but that's not in SDK 1.1, so we do it the slow way */
Image copy = c.createImage(img.getWidth(null), img.getHeight(null));
Graphics g = copy.getGraphics();
......@@ -81,12 +86,12 @@ public class Renderer {
/**
* Flushes all events.
*/
public void flush() {
public synchronized void flush() {
items.removeAllElements();
dirty = true;
}
public boolean isDirty() {
public synchronized boolean isDirty() {
return dirty;
}
}
......@@ -49,6 +49,7 @@ public class Cortado extends Applet implements Runnable, MouseMotionListener,
private int bufferHigh;
private int debug;
private double durationParam;
private double startTimeParam;
private boolean statusRunning;
private Thread statusThread;
public Status status;
......@@ -104,6 +105,8 @@ public class Cortado extends Applet implements Runnable, MouseMotionListener,
"Is this a live stream (disabled PAUSE) (auto|true|false) (default auto)"},
{"duration", "float",
"Total duration of the file in seconds (default unknown)"},
{"startTime", "float",
"Start time of the file in seconds (default unknown)"},
{"audio", "boolean", "Enable audio playback (default true)"},
{"video", "boolean", "Enable video playback (default true)"},
{"kateIndex", "boolean", "Enable playback of a particular Kate stream (default -1 (none))"},
......@@ -233,6 +236,7 @@ public class Cortado extends Applet implements Runnable, MouseMotionListener,
seekable = getEnumParam("seekable", autoBoolVals, "auto");
live = getEnumParam("live", autoBoolVals, "auto");
durationParam = getDoubleParam("duration", -1.0);
startTimeParam = getDoubleParam("startTime", -1.0);
audio = getBoolParam("audio", true);
video = getBoolParam("video", true);
kateIndex = getIntParam("kateIndex", -1);
......@@ -315,19 +319,27 @@ public class Cortado extends Applet implements Runnable, MouseMotionListener,
status.setSeekable(false);
}
if (durationParam < 0) {
if (durationParam < 0 || startTimeParam < 0) {
try {
String base = documentBase != null ? documentBase.toString().substring(0, documentBase.toString().lastIndexOf("/")) : "";
String docurlstring = (urlString.indexOf("://") >= 0) ? urlString : base + "/" + urlString;
Debug.log(Debug.INFO, "trying to determine duration for " + docurlstring);
URL url = new URL(docurlstring);
duration = durationParam = new DurationScanner().getDurationForURL(url, userId, password);
Debug.log(Debug.INFO, "Determined stream duration to be approx. " + durationParam);
DurationScanner dscanner = new DurationScanner();
DurationScanner.TimingInfo tinfo = dscanner.scanURL(url, userId, password);
if (durationParam < 0) {
duration = durationParam = tinfo.duration;
}
if (startTimeParam < 0) {
startTimeParam = tinfo.startTime;
}
Debug.log(Debug.INFO, "Determined stream duration to be approx. " + durationParam+", starting at "+startTimeParam);
} catch (Exception ex) {
Debug.log(Debug.WARNING, "Couldn't determine duration for stream.");
}
}
status.setStartTime(startTimeParam);
status.setDuration(durationParam);
inStatus = false;
mayHide = (hideTimeout == 0);
......
......@@ -241,6 +241,7 @@ public class CortadoPipeline extends Pipeline implements PadListener, CapsListen
}
oksinkpad = fakesink.getPad("sink");
add(fakesink);
fakesink.setState (PAUSE);
}
kselector = ElementFactory.makeByName("selector", "selector");
......
......@@ -49,6 +49,13 @@ public class DurationScanner {
private Page og = new Page();
private Packet op = new Packet();
public class TimingInfo {
public float startTime = -1;
public float duration = -1;
TimingInfo(float st, float d) { startTime = st; duration = d; }
TimingInfo() { startTime = -1; duration = -1; }
};
public DurationScanner() {
oy.init();
}
......@@ -116,6 +123,14 @@ public class DurationScanner {
int ret;
Class c;
if (info.decoder != null) {
ret = info.decoder.takeHeader(packet);
if (ret > 0) {
info.ready = true;
}
return;
}
// try theora
try {
c = Class.forName("com.fluendo.plugin.TheoraDec");
......@@ -147,7 +162,8 @@ public class DurationScanner {
info.type = UNKNOWN;
}
public float getDurationForBuffer(byte[] buffer, int bufbytes) {
public TimingInfo scanBuffer(byte[] buffer, int bufbytes) {
long start = -1;
long time = -1;
int offset = oy.buffer(bufbytes);
......@@ -171,12 +187,19 @@ public class DurationScanner {
while (info.streamstate.packetout(op) == 1) {
int type = info.type;
if (type == NOTDETECTED) {
if (type == NOTDETECTED || !info.ready) {
determineType(op, info);
}
else if (type != NOTDETECTED && type != UNKNOWN && info.ready && info.startgranule < 0) {
info.startgranule = og.granulepos();
long thisStartTime = info.decoder.granuleToTime(info.startgranule);
if (start < 0 || thisStartTime < start) {
start = thisStartTime;
}
Debug.info("start granule for stream "+og.serialno()+": "+info.startgranule);
}
switch (type) {
if (info.ready) switch (type) {
case VORBIS:
{
com.fluendo.plugin.OggPayload pl = info.decoder;
......@@ -199,14 +222,16 @@ public class DurationScanner {
}
}
return time / (float)com.fluendo.jst.Clock.SECOND;
return new TimingInfo(start / (float)com.fluendo.jst.Clock.SECOND,
time / (float)com.fluendo.jst.Clock.SECOND);
}
public float getDurationForURL(URL url, String user, String password) {
public TimingInfo scanURL(URL url, String user, String password) {
try {
int headbytes = 24 * 1024;
int headbytes = 64 * 1024;
int tailbytes = 128 * 1024;
float start = -1;
float time = 0;
byte[] buffer = new byte[1024];
......@@ -218,38 +243,46 @@ public class DurationScanner {
// read beginning of the stream
while (totalbytes < headbytes && read > 0) {
totalbytes += read;
float t = getDurationForBuffer(buffer, read);
time = t > time ? t : time;
TimingInfo tinfo = scanBuffer(buffer, read);
if (tinfo.duration >= 0) {
float t = tinfo.duration;
time = t > time ? t : time;
}
if (tinfo.startTime >= 0 && start < 0) {
start = tinfo.startTime;
}
read = is.read(buffer);
}
is.close();
is = openWithConnection(url, user, password, contentLength - tailbytes);
if(responseOffset == 0) {
is = openWithConnection(url, user, password, Math.max(0, contentLength - tailbytes));
if(responseOffset == 0 && tailbytes<contentLength) {
Debug.warning("DurationScanner: Couldn't complete duration scan due to failing range requests!");
return -1;
return new TimingInfo();
}
read = is.read(buffer);
// read tail until eos, also abort if way too many bytes have been read
while (read > 0 && totalbytes < (headbytes + tailbytes) * 2) {
totalbytes += read;
float t = getDurationForBuffer(buffer, read);
time = t > time ? t : time;
TimingInfo tinfo = scanBuffer(buffer, read);
if (tinfo.duration >= 0) {
time = tinfo.duration > time ? tinfo.duration : time;
}
read = is.read(buffer);
}
return time;
return new TimingInfo(start, time);
} catch (IOException e) {
Debug.error(e.toString());
return -1;
return new TimingInfo();
}
}
private class StreamInfo {
public com.fluendo.plugin.OggPayload decoder;
public com.fluendo.plugin.OggPayload decoder = null;
public int type = NOTDETECTED;
public long startgranule;
public long startgranule = -1;
public StreamState streamstate;
public boolean ready = false;
}
......@@ -259,8 +292,8 @@ public class DurationScanner {
URL url;
url = new URL(args[0]);
System.out.println(new DurationScanner().getDurationForURL(url, null, null));
DurationScanner ds = new DurationScanner();
TimingInfo tinfo = ds.scanURL(url, null, null);
System.out.println(tinfo.duration);
}
}
......@@ -76,6 +76,7 @@ public class Status extends Component implements MouseListener,
private double position = 0;
private long time;
private double startTime = 0;
private double duration;
private long byteDuration;
private long bytePosition;
......@@ -402,13 +403,14 @@ public class Status extends Component implements MouseListener,
if (clicked == NONE) {
double newPosition;
if (seconds < duration || seekable)
if (seconds < duration || seekable) {
time = (long) seconds;
}
else
time = (long) duration;
if(duration > -1) {
newPosition = ((double) time) / duration;
newPosition = ((double) time - startTime) / duration;
if (newPosition != position) {
position = newPosition;
component.repaint();
......@@ -421,6 +423,11 @@ public class Status extends Component implements MouseListener,
}
}
public void setStartTime(double seconds) {
startTime = seconds >= 0 ? seconds : 0;
component.repaint();
}
public void setDuration(double seconds) {
duration = seconds;
component.repaint();
......@@ -641,7 +648,7 @@ public class Status extends Component implements MouseListener,
if (newPosition != position) {
position = newPosition;
time = (long) (duration * position);
time = (long) (startTime + duration * position);
component.repaint();
}
}
......
......@@ -19,6 +19,7 @@
package com.fluendo.plugin;
import java.awt.*;
import java.util.*;
import java.awt.image.*;
import com.fluendo.jst.*;
import com.fluendo.jtiger.Renderer;
......@@ -32,6 +33,184 @@ public class KateOverlay extends Overlay
private Renderer tr = new Renderer();
private Dimension image_dimension = null;
/* This class allows lazy rendering, which may not even happen
if the buffer is late, saving cycles, and ensuring buffers are
not delayed on their way to the sink */
private class OverlayProducer implements ImageProducer, ImageConsumer {
private Vector consumers;
private Component component;
private Renderer tr;
private Buffer buf;
private java.lang.Object object;
OverlayProducer(Component c, Renderer tr, Buffer b) {
consumers = new Vector();
component = c;
this.tr = tr;
this.buf = b;
object = buf.object;
}
public void addConsumer(ImageConsumer ic) {
if (!isConsumer(ic)) consumers.add(ic);
}
public boolean isConsumer(ImageConsumer ic) {
return consumers.contains(ic);
}
public void removeConsumer(ImageConsumer ic) {
consumers.remove(ic);
ImageProducer ip = (ImageProducer)object;
for (int n=0; n<consumers.size(); ++n) {
ip.removeConsumer(ic);
}
}
public void requestTopDownLeftRightResend(ImageConsumer ic) {
}
public void startProduction(ImageConsumer ic) {
Image img = null;
addConsumer(ic);
if (image_dimension == null) {
img = getImage(object);
if (img == null) {
sendError();
return;
}
image_dimension = new Dimension(img.getWidth(null), img.getHeight(null));
}
/* before rendering, we update the state of the events; for now this
just weeds out old ones, but at some point motions could be tracked. */
int ret = tr.update(component, image_dimension, buf.timestamp/(double)Clock.SECOND);
if (ret < 0) {
Debug.log(Debug.WARNING, "Failed to update jtiger renderer");
sendOriginalImage();
return;
}
/* if the renderer is empty and the buffer is not a duplicate, we leave the
video alone */
if (!buf.duplicate && ret > 0) {
Debug.log(Debug.DEBUG, "Video frame is not a dupe and we have nothing to overlay.");
sendOriginalImage();
return;
}
/* if the renderer isn't dirty and the image hasn't changed, we don't need
to do anything, as the result image would be the same */
if (buf.duplicate && !tr.isDirty()) {
Debug.log(Debug.DEBUG, "Video frame is a dupe and we're not dirty. Yeah.");
sendOriginalImage();
return;
}
/* render Kate stream on top */
if (img == null) {
img = getImage(object);
}
img = tr.render(component, img);
/* We need to draw a new overlay, so we need to get the buffer to update,
as it might have a previous overlay on top of it */
buf.duplicate = false;
sendImage(img);
}
private Image getImage(java.lang.Object object) {
Image img;
if (object instanceof ImageProducer) {
img = component.createImage((ImageProducer)object);
}
else if (object instanceof Image) {
img = (Image)object;
}
else {
System.out.println(this+": unknown buffer received "+object);
img = null;
}
return img;
}
/* tells the consumers there was an error producing the image */
private void sendError() {
Debug.log(Debug.WARNING, "Sending image error notification");
for (int n=0; n<consumers.size(); ++n) {
ImageConsumer ic = (ImageConsumer)consumers.elementAt(n);
ic.imageComplete(ImageConsumer.IMAGEERROR);
}
}
/* sends the original image, unmodified, to the consumers, by forwarding all
ImageConsumer calls from the original image to our own consumers */
private void sendOriginalImage() {
ImageProducer ip = (ImageProducer)object;
ip.startProduction(this);
}
/* sends the given image to the consumers */
private void sendImage(Image img) {
PixelGrabber pg = new PixelGrabber(img, 0, 0, -1, -1, false);
try {
if (pg.grabPixels(0)) {
int[] pixels = (int[])pg.getPixels();
if (pixels == null) {
Debug.log(Debug.WARNING, "pixels are null!");
sendError();
}
else {
for (int n=0; n<consumers.size(); ++n) {
ImageConsumer ic = (ImageConsumer)consumers.elementAt(n);
ic.setHints(ImageConsumer.TOPDOWNLEFTRIGHT |
ImageConsumer.COMPLETESCANLINES |
ImageConsumer.SINGLEFRAME |
ImageConsumer.SINGLEPASS);
ic.setDimensions(image_dimension.width, image_dimension.height);
ic.setPixels(0, 0, image_dimension.width, image_dimension.height, pg.getColorModel(), pixels, 0, image_dimension.width);
ic.imageComplete(ImageConsumer.STATICIMAGEDONE);
}
}
}
else {
Debug.log(Debug.WARNING, "Failed to grab pixels");
sendError();
}
}
catch (Exception e) {
Debug.log(Debug.WARNING, "Failed to grab pixels: "+e.toString());
sendError();
}
}
/* ImageConsumer interface, to redirect calls from the original image */
public void imageComplete(int status) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).imageComplete(status);
}
public void setColorModel(ColorModel cm) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).setColorModel(cm);
}
public void setDimensions(int w, int h) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).setDimensions(w, h);
}
public void setHints(int hints) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).setHints(hints);
}
public void setProperties(java.util.Hashtable props) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).setProperties(props);
}
public void setPixels(int x, int y, int w, int h, ColorModel model, byte[] pixels, int off, int scansize) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).setPixels(x, y, w, h, model, pixels, off, scansize);
}
public void setPixels(int x, int y, int w, int h, ColorModel model, int[] pixels, int off, int scansize) {
for (int n=0; n<consumers.size(); ++n) ((ImageConsumer)consumers.elementAt(n)).setPixels(x, y, w, h, model, pixels, off, scansize);
}
};
private Pad kateSinkPad = new Pad(Pad.SINK, "katesink") {
protected boolean eventFunc (com.fluendo.jst.Event event) {
/* don't propagate, the video sink is the master */
......@@ -88,68 +267,11 @@ public class KateOverlay extends Overlay
Debug.log(Debug.DEBUG, "Kate overlay flushing");
}
protected Image getImage(com.fluendo.jst.Buffer buf) {
Image img;
if (buf.object instanceof ImageProducer) {
img = component.createImage((ImageProducer)buf.object);
}<