轉載自:嚴健康的我的博客 http://www.yanjiankang.cn/javascript
本文連接地址: http://www.yanjiankang.cn/rtsp_camera_to_web_browser/html
最近在作一個流媒體的項目,項目中須要採集攝像頭的視頻流到網頁界面實時播放,通常ip攝像頭的流格式都是rtsp的,雖然能夠經過vlc實時播放,可是不如瀏覽器觀看給用戶的體驗簡單。html5
根據查找的資料和實際的實踐,目前發現的切實可行的方案有如下幾種(由於項目是採用java開發,所以下面的代碼也主要使用java實現):java
使用xuggle庫直接解碼rtsp流,解碼結果直接發送給rtmp服務器,而後瀏覽器使用flash直接播放rtmp的視頻流;node
ffmpeg直接解碼rtsp流,將解碼結果使用http發送到nodejs服務器,nodejs服務器使用websocket發送給客戶端,客戶端使用canvas實時繪製圖像;linux
下面詳細介紹這幾種方案。git
FFmpeg是一個自由軟件,能夠運行音頻和視頻多種格式的錄影、轉換、流功能,包含了libavcodec——這是一個用於多個項目中音頻和視頻的解碼器庫,以及libavformat——一個音頻與視頻格式轉換庫。
FFmpeg的安裝請參考:ubuntu上安裝ffmpeggithub
xuggle官網是一個開源的資源庫,可以讓開發者更好的去對視頻和音頻文件進行解碼、編碼、以及錄製等功能。xuggle是對ffmepg的封裝,是一套基於ffmpeg的開發庫。 使用很是方便。 在java中使用時,請在eclipse中導入其jar包。 xuggle-5.4-jar包下載web
xuggle讀取rtsp攝像頭的代碼以下:
import包以下:npm
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 便可查看到實時視頻。
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
|