转载自:严健康的个人博客 http://www.yanjiankang.cn/
本文链接地址: http://www.yanjiankang.cn/rtsp_camera_to_web_browser/
最近在做一个流媒体的项目,项目中需要采集摄像头的视频流到网页界面实时播放,一般ip摄像头的流格式都是rtsp的,虽然可以通过vlc实时播放,但是不如浏览器观看给用户的体验简单。
根据查找的资料和实际的实践,目前发现的切实可行的方案有以下几种(因为项目是采用java开发,因此下面的代码也主要使用java实现):
- 使用xuggle库直接解码rtsp流,将解码的一帧帧图片发送给浏览器,使用html5的video播放mjpeg格式,即图片的不断刷新;
-
使用xuggle库直接解码rtsp流,解码结果直接发送给rtmp服务器,然后浏览器使用flash直接播放rtmp的视频流;
-
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 元素上。
- 安装nodejs
1
2
3
4
|
sudo
apt
-
get
install
nodejs
sudo
apt
-
get
install
npm
sudo
apt
-
get
install
node
-
legacy
|
- 下载 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/
|
- 服务器启动后,你就可以启动 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 中的长和宽也需要正确指定,否则服务器无法正确判断当前的分辨率。
- 要观看直播,需要从前文提到的 jsmpeg 项目中下载 stream-example.html 和 jsmpg.js 文件,更改 stream-example.html 中的 WebSocket URL 为你的服务器地址,并使用你喜欢的浏览器打开。
如果一切正常,你就能看到摄像头画面。
总结
以上三种方案得到的结果都会有延时,具体的分析未完待续。