本文旨在实现使用摄像头采集视频,并且可以在网页实时显示,主要参考的两篇博文为: 
1.  视频实时显示的三种方案  
2.  使用WebSockets进行HTML5视频直播  
我们使用博文1介绍的第三种方案,将摄像头采集到的视频流使用ffmpeg进行编码,并且将其推送给远程服务器,在远程服务器使用stream-server.js处理视频流并且在网页上显示出来。由于原博文介绍详尽,本处不再赘述。 
注意 : 
1. 客户端电脑中需要预先安装ffmpeg,安装方案谷歌即可。

2. 博文1中第三种方案安装nodejs过程中sudo apt-get install node-legacy 应该改为sudo apt-get install nodejs-legacy。

最近在做一个流媒体的项目,项目中需要采集摄像头的视频流到网页界面实时播放,一般ip摄像头的流格式都是rtsp的,虽然可以通过vlc实时播放,但是不如浏览器观看给用户的体验简单。

根据查找的资料和实际的实践,目前发现的切实可行的方案有以下几种(因为项目是采用java开发,因此下面的代码也主要使用java实现):

  1. 使用xuggle库直接解码rtsp流,将解码的一帧帧图片发送给浏览器,使用html5的video播放mjpeg格式,即图片的不断刷新;
  2. 使用xuggle库直接解码rtsp流,解码结果直接发送给rtmp服务器,然后浏览器使用flash直接播放rtmp的视频流;

  3. ffmpeg直接解码rtsp流,将解码结果使用http发送到nodejs服务器,nodejs服务器使用websocket发送给客户端,客户端使用canvas实时绘制图像;

下面详细介绍这几种方案。

FFmpeg和Xuggle的简介

FFmpeg是一个自由软件,可以运行音频和视频多种格式的录影、转换、流功能,包含了libavcodec——这是一个用于多个项目中音频和视频的解码器库,以及libavformat——一个音频与视频格式转换库。
FFmpeg的安装请参考:ubuntu上安装ffmpeg

xuggle官网是一个开源的资源库,能够让开发者更好的去对视频和音频文件进行解码、编码、以及录制等功能。xuggle是对ffmepg的封装,是一套基于ffmpeg的开发库。 使用非常方便。 在java中使用时,请在eclipse中导入其jar包。 xuggle-5.4-jar包下载

方案一的具体实现

xuggle读取rtsp摄像头的代码如下:
import包如下:

1
2
3
4
5

import com.xuggle.mediatool.IMediaReader;
import com.xuggle.mediatool.MediaListenerAdapter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.mediatool.event.IVideoPictureEvent;

其中:streamLocation是需要读取的rtsp地址

1
2
3
4
5
6

mediaReader = ToolFactory.makeReader(streamLocation);
mediaReader.setBufferedImageTypeToGenerate(BufferedImage.TYPE_3BYTE_BGR);
mediaReader.addListener(this);
while (mediaReader.readPacket() == null && running ) ;
mediaReader.close();

上面这段代码实现了rtsp的持续读取,那么读取到的数据怎么获取,很简单,实现以下的帧回调函数onVideoPicture即可。

1
2
3
4
5
6
7
8
9
10

/**
* Gets called when FFMPEG transcoded a frame
*/
public void onVideoPicture(IVideoPictureEvent event) {
    BufferedImage frame = event.getImage();
    //stream是用户自定义的rtsp视频流的id,例如;streamId = this.hashcode()即可
    images.put(streamId ,frame);
    frameNr++;
}

以上的images是使用guava库的Cache建立的代码如下:

1
2
3
4
5

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
private static Cache<String, BufferedImage> images = null;

使用html5 video标签+javax.ws.rs实现的网页显示代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254

import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import javax.imageio.ImageIO;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.StreamingOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.Cache;
import com.sun.jersey.api.container.httpserver.HttpServerFactory;
import com.sun.jersey.api.core.ApplicationAdapter;
import com.sun.net.httpserver.HttpServer;
/**
* A simple webservice used to view results MJPEG streams.
* The webservice supports a number of calls different calls:
* <ol>
* <li>http://IP:PORT/streaming/streams : lists the available JPG and MJPEG urls
* </li>
* <li>http://IP:PORT/streaming/picture/{streamid}.jpg : url to grab jpg
* pictures</li>
* <li>http://IP:PORT/streaming/tiles : provides a visual overview of all the
* streams available at this service. Clicking an image will open the mjpeg
* stream</li>
* <li>http://IP:PORT/streaming/mjpeg/{streamid}.mjpeg : provides a possibly
* never ending mjpeg formatted stream</li>
* </ol>
*
* The service runs on port 8558 by default but this can be changed by using the
* port(int) method.
*
* @author Corne Versloot
*
*/
@Path("/streaming")
public class MjpegStreamingOp extends Application {
    private static Cache<String, BufferedImage> images = null;
    private Logger logger = LoggerFactory.getLogger(getClass());
    private HttpServer server;
    private int port = 8558;
    private int frameRate = 20;  // this parameter decide the sleep time
    public MjpegStreamingOp port(int nr) {
        this.port = nr;
        return this;
    }
    public MjpegStreamingOp framerate(int nr) {
        this.frameRate = nr;
        return this;
    }
    public void prepare() throws IllegalArgumentException, IOException {
        //此处需要修改为从rtsp读进来的images存放的类
        images = TCPClient.getImages();
        ApplicationAdapter connector = new ApplicationAdapter(
                new MjpegStreamingOp());
        server = HttpServerFactory.create("http://localhost:" + port + "/",
                connector);
        server.start();
    }
    /**
     * Sets the classes to be used as resources for this application
     */
    public Set<Class<?>> getClasses() {
        Set<Class<?>> s = new HashSet<Class<?>>();
        s.add(MjpegStreamingOp.class);
        return s;
    }
    public void deactivate() {
        server.stop(0);
        images.invalidateAll();
        images.cleanUp();
    }
    @GET
    @Path("/streams")
    @Produces("text/plain")
    public String getStreamIds() throws IOException {
        String result = new String();
        for (String id : images.asMap().keySet()) {
            result += "/streaming/picture/" + id + ".jpeg\r\n";
        }
        System.out.println("\r\n");
        for (String id : images.asMap().keySet()) {
            result += "/streaming/mjpeg/" + id + ".mjpeg\r\n";
        }
        return result;
    }
    @GET
    @Path("/picture/{streamid}.jpeg")
    @Produces("image/jpg")
    public Response jpeg(@PathParam("streamid") final String streamId) {
        BufferedImage image = null;
        if ((image = images.getIfPresent(streamId)) != null) {
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            try {
                ImageIO.write(image, "jpg", baos);
                byte[] imageData = baos.toByteArray();
                return Response.ok(imageData).build(); // non streaming
                // return Response.ok(new
                // ByteArrayInputStream(imageDAta)).build(); // streaming
            } catch (IOException ioe) {
                logger.warn("Unable to write image to output", ioe);
                return Response.serverError().build();
            }
        } else {
            return Response.noContent().build();
        }
    }
    @GET
    @Path("/playmultiple")
    @Produces("text/html")
    public String showPlayers(@DefaultValue("3") @QueryParam("cols") int cols,
            @DefaultValue("0") @QueryParam("offset") int offset,
            @DefaultValue("6") @QueryParam("number") int number)
            throws IOException {
        // number = Math.min(6, number);
        String result = "<html><head><title>Mjpeg stream players
                        </title></head><body bgcolor=\"#3C3C3C\">";
        result += "<font style=\"color:#CCC;\">Streams: " + images.size()
                + " (showing " + offset + " - "
                + Math.min(images.size(), offset + number) + ")</font><br/>";
        result += "<table style=\"border-spacing:0;
                   border-collapse: collapse;\"><tr>";
        int videoNr = 0;
        for (String id : images.asMap().keySet()) {
            if (videoNr < offset) {
                videoNr++;
                continue;
            }
            if (videoNr - offset > 0 && (videoNr - offset) % cols == 0) {
                result += "</tr><tr>";
            }
            result += "<td><video poster=\"mjpeg/"
                    + id
                    + ".mjpeg\">"
                    + "Your browser does not support the video tag.</video></td>";
            // result +=
            // "<td><img src=\"http://"+InetAddress.getLocalHost().getHostAddress()
                             +":"+port+"/streaming/mjpeg/"+id+".mjpeg\"></td>";
            if (videoNr > offset + number)
                break;
            videoNr++;
        }
        result += "</tr></table></body></html>";
        return result;
    }
    @GET
    @Path("/play")
    @Produces("text/html")
    public String showPlayers(@QueryParam("streamid") String streamId)
            throws IOException {
        String result = "<html><head><title>Mjpeg stream: " + streamId
                + "</title></head><body bgcolor=\"#3C3C3C\">";
        result += "<font style=\"color:#CCC;\"><a href=\"tiles\">Back</a></font><br/>";
        result += "<table style=\"border-spacing:0; border-collapse: collapse;\"><tr>";
        result += "<video poster=\"mjpeg/" + streamId + ".mjpeg\">"
                + "Your browser does not support the video tag.</video>";
        return result;
    }
    @GET
    @Path("/tiles")
    @Produces("text/html")
    public String showTiles(@DefaultValue("3") @QueryParam("cols") int cols,
            @DefaultValue("-1") @QueryParam("width") float width)
            throws IOException {
        String result = "<html><head><title>Mjpeg stream players</title>";
        result += "</head><body bgcolor=\"#3C3C3C\">";
        result += "<table style=\"border-spacing:0; border-collapse: collapse;\"><tr>";
        int videoNr = 0;
        for (String id : images.asMap().keySet()) {
            if (videoNr > 0 && videoNr % cols == 0) {
                result += "</tr><tr>";
            }
            result += "<td><a href=\"play?streamid=" + id
                    + "\"><img src=\"picture/" + id + ".jpeg\" "
                    + (width > 0 ? "width=\"" + width + "\"" : "") + "/></a>";
            videoNr++;
        }
        result += "</tr></table></body></html>";
        return result;
    }
    @GET
    @Path("/mjpeg/{streamid}.mjpeg")
    @Produces("multipart/x-mixed-replace; boundary=--BoundaryString\r\n")
    public Response mjpeg(@PathParam("streamid") final String streamId) {
        StreamingOutput output = new StreamingOutput() {
            private BufferedImage prevImage = null;
            private int sleep = 1000 / frameRate;
            @Override
            public void write(OutputStream outputStream) throws IOException,
                    WebApplicationException {
                BufferedImage image = null;
                try {
                    while ((image = images.getIfPresent(streamId)) != null)
                        if (prevImage == null || !image.equals(prevImage)) {
                            ByteArrayOutputStream baos = new ByteArrayOutputStream();
                            ImageIO.write(image, "jpg", baos);
                            byte[] imageData = baos.toByteArray();
                            outputStream
                                    .write(("--BoundaryString\r\n"
                                            + "Content-type: image/jpeg\r\n"
                                            + "Content-Length: "
                                            + imageData.length + "\r\n\r\n")
                                            .getBytes());
                            outputStream.write(imageData);
                            outputStream.write("\r\n\r\n".getBytes());
                            outputStream.flush();
                        }
                        Thread.sleep(sleep);
                    }
                    outputStream.flush();
                    outputStream.close();
                } catch (IOException ioe) {
                    logger.info("Steam for [" + streamId
                            + "] closed by client!");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };
        return Response.ok(output).header("Connection", "close")
                .header("Max-Age", "0").header("Expires", "0")
                .header("Cache-Control", "no-cache, private")
                .header("Pragma", "no-cache").build();
    }
}

以上代码参考自github上stormcv项目,项目地址为:https://github.com/sensorstorm/StormCV, 感谢原作者。

至此,打开网页 http://localhost:8558/streaming/tiles 即可查看到实时视频。

方案二的具体实现

xuggle库转rtsp为rtmp

rtsp的reader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xuggle.mediatool.IMediaReader;
import com.xuggle.mediatool.MediaListenerAdapter;
import com.xuggle.mediatool.ToolFactory;
import com.xuggle.mediatool.event.IVideoPictureEvent;
import com.xuggle.xuggler.ICodec;
import com.xuggle.xuggler.IContainer;
import com.xuggle.xuggler.IVideoPicture;
/**
* This class reads a video stream or file, decodes frames and transcode to rtmp stream
* @author jkyan
*/
public class RTSPReader extends MediaListenerAdapter implements Runnable {
    private Logger logger = LoggerFactory.getLogger(RTSPReader.class);
    private IMediaReader mediaReader;
    private int frameSkip;
    private int groupSize;
    private long frameNr; // number of the frame read so far
    private boolean running = false;
    // used to determine if the EOF was reached if Xuggler does not detect it
    private long lastRead = -1;
    private int sleepTime = 0;
    private String streamName;
    private String streamLocation = null;
    private RTMPWriter rtmpWriter = null;
    Double frameRate = 0.0;
    // frameskip和groupsize决定了需要读取的帧的数目,即采样率,
    // 例如,1,1时代表每一帧都需要读取,10,5时代表只需要[0 1 2 3 4] [10 11 12 13 14] ...
    public RTSPReader(String streamName, String streamLocation,
        int frameSkip, int groupSize, int sleepTime) {
        this.streamLocation = streamLocation;
        this.frameSkip = Math.max(1, frameSkip);
        this.groupSize = Math.max(1, groupSize);
        this.sleepTime = sleepTime;
        this.streamName = streamName;
        lastRead = System.currentTimeMillis() + 10000;
    }
    /**
     * Start reading the provided URL
     */
    public void run() {
        running = true;
        while (running) {
            try {
                // if a url was provided read it
                if (streamLocation != null) {
                    logger.info("Start reading stream: " + streamLocation);
                    mediaReader = ToolFactory.makeReader(streamLocation);
                    lastRead = System.currentTimeMillis() + 10000;
                    frameNr = 0;
                    rtmpWriter = new RTMPWriter(576,704,30.0,streamName);
                    mediaReader.addListener(this);
                    while (mediaReader.readPacket() == null && running);
                    // reset internal state
                    rtmpWriter.setFinish();
                    mediaReader.close();
                } else {
                    logger.error("No stream provided, nothing to read");
                    break;
                }
            } catch (Exception e) {
                logger.warn("Stream closed unexpectatly: " + e.getMessage(), e);
                // sleep a minute and try to read the stream again
                sleep(60 * 1000);
            }
        }
        running = false;
    }
    public void sleep(int ms) {
        try {
            Thread.sleep(ms);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    /**
     * Gets called when FFMPEG transcoded a frame
     */
    public void onVideoPicture(IVideoPictureEvent event) {
        lastRead = System.currentTimeMillis();
        if (frameNr % frameSkip < groupSize) {
            IVideoPicture picture = event.getPicture();
            rtmpWriter.write(frameNr, picture);
            // enforced throttling
            if (sleepTime > 0)
                sleep(sleepTime);
        }
        frameNr++;
    }
    /**
     * Tells the StreamReader to stop reading frames
     */
    public void stop() {
        running = false;
    }
    /**
     * Returns whether the StreamReader is still active or not
     * @return
     */
    public boolean isRunning() {
        // kill this thread if the last frame read is to long ago
        // (means Xuggler missed the EoF) and clear resources
        if (lastRead > 0 && System.currentTimeMillis() - lastRead > 3000) {
            running = false;
            if (mediaReader != null && mediaReader.getContainer() != null)
                mediaReader.getContainer().close();
            return this.running;
        }
        return true;
    }
}

rtmp服务器的搭建方法请参考这里。

rtmp的writer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

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.IStream;
import com.xuggle.xuggler.IStreamCoder;
import com.xuggle.xuggler.IVideoPicture;
import java.util.Collection;
import java.util.Iterator;
import javax.management.RuntimeErrorException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class RTMPWriter {
    private Logger logger = LoggerFactory.getLogger(RTMPWriter.class);
    // private static String url = "";
    private String url = "rtmp://localhost:1935/live1/";
    private String appName = "";
    private int height = 0;
    private int width = 0;
    private IStreamCoder coder = null;
    private IContainer container = null;
    private Boolean isfinish = false;
    //rtmp writer中需要获取rtsp流图像的宽和高,帧频作为编码参数
    //rtmp服务器的地址格式一般为:ip+端口+地址+应用名称
    @SuppressWarnings("deprecation")
    public RTMPWriter(int height, int width, double frameRate, String appName) {
        container = IContainer.make();
        IContainerFormat containerFormat = IContainerFormat.make();
        containerFormat.setOutputFormat("flv", url + appName, null);
        // set the buffer length xuggle will suggest to ffmpeg for reading inputs
        container.setInputBufferLength(0);
        int retVal = container.open(url + appName, IContainer.Type.WRITE,
                containerFormat);
        if (retVal < 0) {
            logger.error("Could not open output container for live stream");
        } else {
            logger.info("hava opened server " + url + appName + " for write!");
        }
        IStream stream = container.addNewStream(0);
        coder = stream.getStreamCoder();
        ICodec codec = ICodec.findEncodingCodec(ICodec.ID.CODEC_ID_H264);
        if (codec == null) {
            logger.warn("cannot find h264 encoding codec!");
            Collection<ICodec> icodec_collections = ICodec.getInstalledCodecs();
            Iterator<ICodec> iterator = icodec_collections.iterator();
            while (iterator.hasNext()) {
                ICodec icodec = iterator.next();
                logger.info("Your system supports codec:" + icodec.getName());
            }
        } else {
            coder.setCodec(codec);
        }
        coder.setNumPicturesInGroupOfPictures(5);
        coder.setBitRate(256000);
        coder.setPixelType(IPixelFormat.Type.YUV420P);
        if (width == 0 || height == 0) {
            logger.error("cannot get the real size of the stream needed to read");
        }
        coder.setHeight(height);
        coder.setWidth(width);
        coder.setFlag(IStreamCoder.Flags.FLAG_QSCALE, true);
        coder.setGlobalQuality(0);
        IRational rationalFrameRate = IRational.make(frameRate);
        coder.setFrameRate(rationalFrameRate);
        coder.setTimeBase(IRational.make(rationalFrameRate.getDenominator(),
                rationalFrameRate.getNumerator()));
        coder.open();
        logger.info("[ENCODER] address: " + url + appName +
                "\n video size is " + width + "x" + height
                + " and framerate is " + frameRate);
        if (container.writeHeader() < 0)
          throw new RuntimeException("cannot write header");
    }
    public void setFinish() {
        this.isfinish = true;
    }
    public void write(long frameNr, IVideoPicture picture) {
        IPacket packet = IPacket.make();
        if (frameNr == 0) {
            // make first frame keyframe
            picture.setKeyFrame(true);
        }
        picture.setQuality(0);
        coder.encodeVideo(packet, picture, 0);
        picture.delete();
        if (packet.isComplete()) {
            if (container.writePacket(packet) < 0) {
                throw new RuntimeException("cannot write packet");
            } else {
                logger.info("[ENCODER] writing packet of size " + packet.getSize());
            }  
        }
        if (isfinish) {
            if (container.writeTrailer() < 0)
                throw new RuntimeException("cannot write Trailer");
            coder.close();
            container.close();
        }
    }
}

到这里已经实现了rtsp流推送到rtsp的服务器。下一步即为rtmp流的显示。使用的是flowplayer。

FlowPlayer 是一个用Flash开发的在Web上的视频播放器,可以很容易将它集成在任何的网页上。支持HTTP以及流媒体传输。

FlowPlayer的示例程序可以点击此处下载。(FlowPlayer是商业软件,此处仅作测试,请支持正版)

下载flowplay并实现以下的html,即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45

<!DOCTYPE html>
<html>
<head>
<script type="text/javascript" src="flowplayer-3.2.13.min.js"></script>
<title>RTSP Camera to RTMP Player</title>
</head>
<body>  
    <h1>RTSP Camera to RTMP Player</h1>
    <!-- this A tag is where your Flowplayer will be placed. it can be anywhere -->
    <a  
         href="#"
         style="display:block;width:520px;height:330px"  
         id="player">
    </a>
    <!-- this will install flowplayer inside previous A- tag. -->
    <!-- 修改url为rtmp的应用名称,netConnectionUrl为rtmp服务器的地址 -->
    <script>
    flowplayer("player", "flowplayer-3.2.18.swf",{
        clip: {
          url: 'app',
          provider: 'rtmp',
          live: true,
        },  
        plugins: {  
           rtmp: {  
             url: 'flowplayer.rtmp-3.2.13.swf',  
             netConnectionUrl: 'rtmp://localhost:1935/live1'
           }
       }
    });
    </script>
    <p>    
        Sample RTMP URL (Live) is "rtmp://live.hkstv.hk.lxdns.com/live/hks"
    </p>
</body>
</html>

启动rtmp服务器,实例化上面的RTSPReader类并传入相应的rtsp摄像头地址,再打开这个网页即可看到摄像头直播。可以使用VLC打开rtsp流对比观看。
rtsp示例地址: rtsp://streaming3.webcam.nl:1935/n224/n224.stream

另外推荐一个世界各地的摄像头直播网站,很有趣: 世界各地的摄像头

ffmpeg 命令直接 转rtsp为rtmp

上面是使用java库转rtsp为rtmp。
ffmpeg直接就可以通过命令将rtsp转rtmp

1
2

ffmpeg -i rtsp://地址  -f flv  rtmp://localhost:1935/live1/app

方案三的具体实现

这套方案参考地址为:使用 WebSockets 进行 HTML5 视频直播 – SegmentFault

具体实现如下:
来自rtsp摄像头的视频被 ffmpeg 编码,然后通过 HTTP 传递给一个 Node.js 写的小脚本;脚本会将这条 MPEG 视频流通过 WebSockets 分发给所有链接的浏览器;浏览器使用 JavaScript 解码 MPEG 视频流并将解码后的画面渲染到 Canvas 元素上。

  1. 安装nodejs
1
2
3
4

sudo apt-get install nodejs
sudo apt-get install npm
sudo apt-get install node-legacy

  1. 下载 phoboslab/jsmpeg 项目的 stream-server.js 脚本。安装 WebSocket包ws并启动服务器:
1
2
3

npm install ws
node stream-server.js 你的密码

这里的密码是用来确视频流不被劫持用的。如果服务器运行正常,你应该会看到这样的输出:

1
2
3

Listening for MPEG Stream on http://127.0.0.1:8082/<secret>/<width>/<height>
Awaiting WebSocket connections on ws://127.0.0.1:8084/

  1. 服务器启动后,你就可以启动 ffmpeg 并将它指向到正在运行的这个域名和端口了:
1
2

ffmpeg -i rtsp://admin:12345@10.124.142.15:554/h264/ch1/main/av_stream  -f mpeg1video  -r 25  -s 640x480  http://localhost:8082/123456/640/480/

这条命令会开始从rtsp流捕捉 640×480 的视频,并编码成 25fps 码率的 MPEG 视频。编码后的视频会通过 HTTP 被发送到所指定的服务器和端口。确保密码正确,URL 中的长和宽也需要正确指定,否则服务器无法正确判断当前的分辨率。

  1. 要观看直播,需要从前文提到的 jsmpeg 项目中下载 stream-example.html 和 jsmpg.js 文件,更改 stream-example.html 中的 WebSocket URL 为你的服务器地址,并使用你喜欢的浏览器打开。

如果一切正常,你就能看到摄像头画面。

总结

以上三种方案得到的结果都会有延时,具体的分析未完待续。


转载请保留原文链接。本文内容均为实践后的学习整理,本人拥有所有解释权。如本文侵犯了你的版权,请联系作者yanhealth@163.com进行删除。

就 HTML5 来说,视频(实时)直播是一个很悲催的活,HTML5 视频目前还没有一个正式的流式传输支持,Safari 支持很蹩脚的HTTP Live Streaming 并且也即将有 Media Source Extension 规范和 MPEG-DASH。但所有这些方案都是将视频分成小片,由浏览器单独下载,因此会产生最小五秒钟的延迟。

下面是一个完全不同的方案,可以支持所有现代浏览器:Firefox、Chrome、Safari、Mobile Safari、Android 版 Chrome 甚至是 IE10。

原文的这个位置提供了一个伪直播例子。

这套方案向后兼容,没有用到什么新奇技术,目前暂时不支持音频。但它出乎意料地好用。

来自摄像头的视频被 ffmpeg 编码,然后通过 HTTP 传递给一个 Node.js 写的小脚本;脚本会将这条 MPEG 视频流通过 WebSockets 分发给所有链接的浏览器;浏览器使用 JavaScript 解码 MPEG 视频流并将解码后的画面渲染到 Canvas 元素上。

你甚至可以用树莓派来传输视频。可能会有点慢,但是笔者测试过以 30fps 的帧率实时编码 320x240 视频不成问题。对笔者来说这是最好的树莓派视频方案。

下面是构建步骤。首先你需要取得最新版本的 ffmpeg,最新的安装包可以从 deb-multimedia 获得。如果你使用 Linux,你的摄像头应该在位于 /dev/video0 或 /dev/video1;在 OS X 或 Windows 上你可以用 VLC。

确保用来分发视频流的服务器安装了 Node.js。下载 phoboslab/jsmpeg 项目的 stream-server.js 脚本。安装 WebSocket 包 ws 并启动服务器:

npm install ws
node stream-server.js 你的密码

这里的密码是用来确保不会有好奇宝宝来劫持你的视频流用的。如果服务器运行正常,你应该会看到这样的输出:

Listening for MPEG Stream on http://127.0.0.1:8082/<secret>/<width>/<height>
Awaiting WebSocket connections on ws://127.0.0.1:8084/

服务器启动后,你就可以启动 ffmpeg 并将它指向到正在运行的这个域名和端口了:

ffmpeg -s 640x480 -f video4linux2 -i /dev/video0 -f mpeg1video -b 800k -r 30 http://example.com:8082/你的密码/640/480/

这条命令会开始从摄像头捕捉 640x480 的视频,并编码成 30fps 码率 800kbps 的 MPEG 视频。编码后的视频会通过 HTTP 被发送到所指定的服务器和端口。确保密码正确,URL 中的长和宽也需要正确指定,否则服务器无法正确判断当前的分辨率。

在树莓派上你可能需要将分辨率降至 320x240 来确保编码速度仍能维持 30fps。

要观看直播,需要从前文提到的 jsmpeg 项目中下载 stream-example.html 和 jsmpg.js 文件,更改 stream-example.html 中的 WebSocket URL 为你的服务器地址,并使用你喜欢的浏览器打开。

如果一切正常,你就能看到少于 100ms 延迟的流畅的摄像头画面。很好很强大对不?

更便捷的方案请围观原文的 Instant Webcam。

采集rtsp流摄像头到浏览器实时播放方案相关推荐

  1. 【rtsp流在Web端实时播放】使用 VUE + webrtc-steamer

    rtsp流-视频播放 操作系统:Win10 vue版本:vue2 一. 方法 必须将rtsp通过 播放器插件/服务器/- 转换为 flv/webrtc/- 最新在线可用rtsp码流地址(可用flv播放 ...

  2. C#控制摄像头实现画面实时播放

    C#控制摄像头实现画面实时播放 思路说明 关键代码 完整代码下载 思路说明 主要是调用Win32API(avicap32.dll)实现. 关键代码 AviCapture.dll using Syste ...

  3. 海康威视、大华摄像头RTSP视频流嵌入到谷歌Chrome等VUE页面中实时播放方案(图文教程)

    近期在做一个智慧城市项目,要求将海康威视.大华等摄像头RTSP视频流在Chrome.Firefox.Edge等浏览器中播放,并且要求延迟必须要低,能到多低就多低,最好是实时视频. 小编了解很多不同的方 ...

  4. 海康视频VTM流监控浏览器实时播放调试总结

    海康视频VTM流播放调试总结 VTM视频demo调用部分代码 配置好地址账号密码后即可进行浏览器实时调用demo实现 网域ip可在vtm服务配置中获取 设备编码需注意: 设备编码取此处编码设备编号,可 ...

  5. 如何实现监控视频RTSP流在网页中低延时播放

    1.行业痛点 随着平安城市.平安社区.雪亮工程等应用在全国范围的开展,安防视频监控系统得到了大面积使用. 传统的安防应用场景中,监控终端和平台管理端都通过C/S模式实现监控画面的实时查看,电脑端有相应 ...

  6. ffmpeg 找不到bin_FFmpeg开发笔记(九):ffmpeg解码rtsp流并使用SDL同步播放

    若该文为原创文章,转载请注明原文出处 本文章博客地址:https://blog.csdn.net/qq21497936/article/details/109603499 各位读者,知识无穷而人力有穷 ...

  7. 通过LiveNVR拉取海康、大华、华为、天地伟业等各种监控视频RTSP流地址做H5 web播放...

    介绍 LiveNVR是Onvif.RTSP协议的,它是服务端去拉取摄像头的RTSP直播流的,需要与设备在一个局域网, 或者LiveNVR在公网的情况,摄像头设备这边要映射到公网才能拉取到.LiveNV ...

  8. 采集音频和摄像头视频并实时H264编码及AAC编码

    0. 前言 我在前两篇文章中写了DirectShow捕获音视频然后生成avi,再进行264编码的方法.那种方法有一些局限性,不适合实时性质的应用,如:视频会议.视频聊天.视频监控等.本文所使用的技术, ...

  9. 采集音频和摄像头视频并实时H264编码及AAC编码[转]

    0. 前言 我在前两篇文章中写了DirectShow捕获音视频然后生成avi,再进行264编码的方法.那种方法有一些局限性,不适合实时性质的应用,如:视频会议.视频聊天.视频监控等.本文所使用的技术, ...

最新文章

  1. 水仙花数 与 变种水仙花数 的求解 【C语言】
  2. adams建立一绳索不带滑轮_建立企业精益供应链,必须先解开现有绳索 系列(一)...
  3. 使用yum安装gitlab
  4. juniper交换机 mac地址和端口绑定
  5. IOS中UIActionSheet使用方法详解
  6. 第3章:分布式文件系统 HDFS
  7. 前端学习(2263)vue造轮子之webstrom使用
  8. asp.net button创建控件时出错_Tkinter Radiobutton控件
  9. java字符串处理截取和替换字符
  10. 监督计算机控制系统的应用,计算机智能控制系统的应用类型
  11. 扩展内存条 兼容性问题 双通道
  12. 到底程序员的工资有多高?你不了解的程序员!
  13. matlab中的clc命令和clear命令
  14. 文字识别 SDK 11 给大家看一个手册
  15. 下载Synechococcus elongatus UTEX 2973(accession no.为GCA_000817325.1 )的基因组注释文件,统计其中染色体序列(CP006471.1)前10
  16. [转自左潇龙的博客]设计模式大杂烩(24种设计模式的总结以及学习设计模式的几点建议)...
  17. MATLAB中未定义函数或变量”的问题
  18. hive常用函数之其他函数
  19. linux学习好文章,好网站
  20. ADI Blackfin DSP处理器-BF533的开发详解20:4.3寸LCD液晶屏的设计与应用(含源码)

热门文章

  1. html5代码验证电话号码,这个我觉得挺重要的!
  2. 大数据开发超高频面试题!大厂面试必看!包含Hadoop、zookeeper、Hive、flume、kafka、Hbase、flink、spark、数仓等
  3. 两条边延长角会有什么变化_《直线、射线、线段和角》教学设计
  4. android 数据线有几种,不止是安卓和苹果线,手机数据线原来还有这几种!
  5. 再见2018,感谢你的一路陪伴
  6. 电子(自旋、轨道、耦合)磁矩
  7. 怎么去除新装修房子的甲醛
  8. whatsns与tipask_tipask重大安安全漏洞之最佳答案bug修复
  9. tipask 不能正常解析
  10. windows10安装更新很慢ndows,Windows 10升级太慢了?这里有俩窍门