mirror of
https://github.com/Monibuca/plugin-gb28181.git
synced 2025-12-24 13:27:57 +08:00
Compare commits
391 Commits
v1.0.0-alp
...
v4.3.9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
538e96a5c2 | ||
|
|
c2003d53e8 | ||
|
|
64ac75905f | ||
|
|
d2cc62ff9e | ||
|
|
2e8aa47bc5 | ||
|
|
585d5949d3 | ||
|
|
0285236cce | ||
|
|
12895fa2cc | ||
|
|
c66303e7e8 | ||
|
|
5435c2ef1c | ||
|
|
d8c6ad30dd | ||
|
|
86fa7cc7e6 | ||
|
|
692ec21877 | ||
|
|
71f2b36d2d | ||
|
|
b068bd9e5b | ||
|
|
78ac89e7af | ||
|
|
1cec5301c3 | ||
|
|
319f7fc636 | ||
|
|
7a75810203 | ||
|
|
bbc7b09835 | ||
|
|
1dbdff1fe5 | ||
|
|
952c8f0ff8 | ||
|
|
730f3014f8 | ||
|
|
682aec656b | ||
|
|
fbd8683f5b | ||
|
|
b15e4ee89c | ||
|
|
3c7b3a042d | ||
|
|
858df1377e | ||
|
|
60021d3cd9 | ||
|
|
d8061cd7c3 | ||
|
|
ed397063c4 | ||
|
|
5853120d30 | ||
|
|
4c47df0695 | ||
|
|
37fd121d11 | ||
|
|
05fd8c38f7 | ||
|
|
2d85e46a8b | ||
|
|
4a90d7bf91 | ||
|
|
a020f3ea81 | ||
|
|
c68862160f | ||
|
|
9db29a9280 | ||
|
|
5d5dae8939 | ||
|
|
a0f16d1184 | ||
|
|
53ddc0cb63 | ||
|
|
d156974f73 | ||
|
|
f3046bcde3 | ||
|
|
9c970ad282 | ||
|
|
c4de92e9f6 | ||
|
|
cf5a803971 | ||
|
|
f487be5fdb | ||
|
|
bd70d24a16 | ||
|
|
708cd042df | ||
|
|
a69b739e5e | ||
|
|
4e96efa9ff | ||
|
|
3a704b68cc | ||
|
|
c8f51a7ec5 | ||
|
|
b7bad99292 | ||
|
|
7b6b827899 | ||
|
|
d121927c96 | ||
|
|
9a3ad6a51c | ||
|
|
e0c6fbefcd | ||
|
|
b924977085 | ||
|
|
521ee36769 | ||
|
|
583754ea82 | ||
|
|
58b6a818bd | ||
|
|
8b1b176f51 | ||
|
|
68d6cbaab9 | ||
|
|
f88d4d264e | ||
|
|
55aa20e868 | ||
|
|
9f4ad83da7 | ||
|
|
68ff4dba5b | ||
|
|
e9f576e3f4 | ||
|
|
b271cb8e50 | ||
|
|
2b82a0ffc4 | ||
|
|
940d7c5e59 | ||
|
|
2142a474a3 | ||
|
|
86e9bccb85 | ||
|
|
bb3a679a60 | ||
|
|
ecd97c8439 | ||
|
|
bfd71a72d8 | ||
|
|
c6bef8ccd8 | ||
|
|
20c0ac52cb | ||
|
|
34f5b7da79 | ||
|
|
0d3a795dc2 | ||
|
|
b52d457990 | ||
|
|
4a214cebeb | ||
|
|
8f78f992ca | ||
|
|
228d7b0cd2 | ||
|
|
e99150b0be | ||
|
|
31112e0052 | ||
|
|
8663b8e171 | ||
|
|
5960f07fc3 | ||
|
|
eb6004d6ef | ||
|
|
cce5f67ab9 | ||
|
|
fdfb462d46 | ||
|
|
c05adce562 | ||
|
|
aa3727f582 | ||
|
|
6e8709176e | ||
|
|
3e6c43f6ff | ||
|
|
4a7aa94bd2 | ||
|
|
fd13c6d9ab | ||
|
|
085d413d2b | ||
|
|
4eba0e23f9 | ||
|
|
f0324c4283 | ||
|
|
f6b5f15b83 | ||
|
|
a6de68496b | ||
|
|
709988cf09 | ||
|
|
9cf8c1acc1 | ||
|
|
03bd17bd24 | ||
|
|
ed0b751e89 | ||
|
|
b8a2812c40 | ||
|
|
abd7b6ba77 | ||
|
|
a09080641d | ||
|
|
cf761e09d7 | ||
|
|
e4569c1dd7 | ||
|
|
50bfd5b390 | ||
|
|
e937325058 | ||
|
|
158c317511 | ||
|
|
e1a2587b48 | ||
|
|
8f2a90e8ac | ||
|
|
179280b67a | ||
|
|
35c8d5171f | ||
|
|
75342e5bb3 | ||
|
|
c742f226e9 | ||
|
|
11b9da4c9d | ||
|
|
490c85b46a | ||
|
|
5d7d81a46b | ||
|
|
eb53a9594a | ||
|
|
37b4774e9a | ||
|
|
64861c37b1 | ||
|
|
16b5fa375c | ||
|
|
ee419570a2 | ||
|
|
e110acfb56 | ||
|
|
07c4dd0c21 | ||
|
|
0299a9ef7e | ||
|
|
bbeae2bc9c | ||
|
|
b0a23ac3d2 | ||
|
|
7f69c61eaf | ||
|
|
d795e7c80a | ||
|
|
2033bdf04f | ||
|
|
79538986e5 | ||
|
|
438b0d2bd0 | ||
|
|
47c916e131 | ||
|
|
748c4117c6 | ||
|
|
11444a6398 | ||
|
|
ae6cb14536 | ||
|
|
957bdee6a4 | ||
|
|
9cf4607478 | ||
|
|
74241c20ab | ||
|
|
9d7b5feaf0 | ||
|
|
53551f3514 | ||
|
|
6e2bd0c146 | ||
|
|
60f9a18aca | ||
|
|
4bfd3103cc | ||
|
|
98bf9c6b32 | ||
|
|
cfed8a9d2f | ||
|
|
6cd4d8e51a | ||
|
|
dd5617737a | ||
|
|
616df4c2b1 | ||
|
|
b8fcfb0213 | ||
|
|
01c36d591c | ||
|
|
2143cc4911 | ||
|
|
931f8d888d | ||
|
|
c9709d0e48 | ||
|
|
61441cc372 | ||
|
|
803543fbdf | ||
|
|
03a2aeec2c | ||
|
|
8bb92f87f1 | ||
|
|
3b9f9abc4a | ||
|
|
813447b355 | ||
|
|
81827cb8c9 | ||
|
|
4cbffb84b8 | ||
|
|
5b4e520be7 | ||
|
|
cb1bafc269 | ||
|
|
1a013b5ebc | ||
|
|
63eba38094 | ||
|
|
3f03146f5c | ||
|
|
c5ac90cdff | ||
|
|
9c818244cb | ||
|
|
bf5f2b804d | ||
|
|
a2bfeec948 | ||
|
|
3a618fc0ce | ||
|
|
75f3601326 | ||
|
|
c07309f14f | ||
|
|
aefd8e72f5 | ||
|
|
0db5da87a8 | ||
|
|
50da427ab7 | ||
|
|
35c9142d0e | ||
|
|
de4a9668cd | ||
|
|
fc7758e1da | ||
|
|
4cae096425 | ||
|
|
cad56c207d | ||
|
|
75b1890e20 | ||
|
|
83f7d3711a | ||
|
|
b2d65700bc | ||
|
|
eb7e47e98a | ||
|
|
57bce22209 | ||
|
|
97d65b9710 | ||
|
|
416818ff53 | ||
|
|
fd7ee7f80c | ||
|
|
b251f8471a | ||
|
|
a2647a1564 | ||
|
|
8e6544766e | ||
|
|
86f7effd47 | ||
|
|
65c1864d5b | ||
|
|
d474a500c9 | ||
|
|
ba14fdafa7 | ||
|
|
a7c866d453 | ||
|
|
ae6400cfc0 | ||
|
|
bd7f5eba92 | ||
|
|
195c0ac124 | ||
|
|
6fd2d3f5a7 | ||
|
|
523d1e8e5f | ||
|
|
0c4a7356a1 | ||
|
|
ad0402fcbf | ||
|
|
fd2e614d74 | ||
|
|
8704c54137 | ||
|
|
9c99033b3c | ||
|
|
00aa30e428 | ||
|
|
51583d3885 | ||
|
|
837b6b287d | ||
|
|
4e25222df8 | ||
|
|
2f7a6e617d | ||
|
|
cb10ae14bf | ||
|
|
e982c74f1b | ||
|
|
9d59bb33cf | ||
|
|
3341b07812 | ||
|
|
0ea755f462 | ||
|
|
2e71a7115d | ||
|
|
797a59965f | ||
|
|
6555dda300 | ||
|
|
4d401c909f | ||
|
|
84d7f92479 | ||
|
|
4b38e56063 | ||
|
|
a111dafebe | ||
|
|
a5197ed196 | ||
|
|
4a654a6b07 | ||
|
|
034fdbcfea | ||
|
|
1b8dbea862 | ||
|
|
9ed42a3564 | ||
|
|
3244823fff | ||
|
|
3fbe12edc5 | ||
|
|
a7ea95e419 | ||
|
|
4de15502ff | ||
|
|
348676428b | ||
|
|
0676b08d58 | ||
|
|
6b0b31230f | ||
|
|
db7a186647 | ||
|
|
e76d1b7d86 | ||
|
|
8eff1b3373 | ||
|
|
8af13e0b4a | ||
|
|
ca0b019be7 | ||
|
|
bd2dd4c5c3 | ||
|
|
518b20e145 | ||
|
|
aee38b5053 | ||
|
|
5682750723 | ||
|
|
609b9d0f9a | ||
|
|
8c248fc0c0 | ||
|
|
51d8a8fa91 | ||
|
|
1105d732ae | ||
|
|
72be771799 | ||
|
|
5cae3bd5c4 | ||
|
|
686ce753a3 | ||
|
|
0f3fda9159 | ||
|
|
0fcd23cee0 | ||
|
|
28f5774073 | ||
|
|
e813086907 | ||
|
|
8a8d97c676 | ||
|
|
356142a2db | ||
|
|
0c9712e1e5 | ||
|
|
f22a6303c6 | ||
|
|
53bb479588 | ||
|
|
aea30f751a | ||
|
|
4b33a4c8a3 | ||
|
|
0a0f58b1db | ||
|
|
4528a67e92 | ||
|
|
87ac220de1 | ||
|
|
640c310a77 | ||
|
|
53bb4244db | ||
|
|
6b193a4208 | ||
|
|
bf54332376 | ||
|
|
a527903825 | ||
|
|
bb0209fed9 | ||
|
|
23e01d4ccc | ||
|
|
c52549b0c2 | ||
|
|
3a00e5dccc | ||
|
|
65d758ed0b | ||
|
|
5cf4b49033 | ||
|
|
04c93225a1 | ||
|
|
709b394af6 | ||
|
|
57e7977eae | ||
|
|
ed0826a35d | ||
|
|
4495bd2d4d | ||
|
|
7d235cdd04 | ||
|
|
6188118ef6 | ||
|
|
0481b71d52 | ||
|
|
6ab0f9d1c8 | ||
|
|
fd3564afe0 | ||
|
|
0d0208f0f7 | ||
|
|
cc47907517 | ||
|
|
8729b32cb2 | ||
|
|
43fa8b3c37 | ||
|
|
c33b93431a | ||
|
|
1c8618076d | ||
|
|
12d069d949 | ||
|
|
779a868821 | ||
|
|
5841faa3cb | ||
|
|
e5f9aa54c1 | ||
|
|
2fac74846a | ||
|
|
fed0b22513 | ||
|
|
2326500086 | ||
|
|
c31d10c349 | ||
|
|
0d1a15f511 | ||
|
|
0f0b36dc3d | ||
|
|
f224a96033 | ||
|
|
3b70a3ee69 | ||
|
|
b1b0bf06f2 | ||
|
|
7c48ad044c | ||
|
|
bcd59cfc0f | ||
|
|
89f133e50e | ||
|
|
b885173222 | ||
|
|
e7e85466bf | ||
|
|
98cc8824f0 | ||
|
|
dbdf66cdef | ||
|
|
66c1182a4d | ||
|
|
07498fbe58 | ||
|
|
ed3cea25ef | ||
|
|
49b465be1b | ||
|
|
8faeab6728 | ||
|
|
5ccebf2479 | ||
|
|
67c37b56a8 | ||
|
|
78b163384f | ||
|
|
a1534f72f8 | ||
|
|
beed7cba2a | ||
|
|
5799281628 | ||
|
|
3ae1805543 | ||
|
|
c5d328da16 | ||
|
|
9ceeb2d511 | ||
|
|
f3ffbb7f3d | ||
|
|
af8829baa2 | ||
|
|
5b8f63a13b | ||
|
|
822f75d36b | ||
|
|
05b8d75155 | ||
|
|
9669085328 | ||
|
|
22a56b02fb | ||
|
|
0f58d9dde6 | ||
|
|
7f9fb67230 | ||
|
|
d25bb3854a | ||
|
|
261bc00de0 | ||
|
|
7cfd4fccbd | ||
|
|
211c8bd32c | ||
|
|
f3b0595863 | ||
|
|
de07b41647 | ||
|
|
38220d62e3 | ||
|
|
818fd6bd33 | ||
|
|
8154d852f4 | ||
|
|
c4a54d7eae | ||
|
|
3ffb58606a | ||
|
|
c284e4e28e | ||
|
|
7269ec50de | ||
|
|
e33079e36b | ||
|
|
d1de189dcf | ||
|
|
e45b266de9 | ||
|
|
8663a9ecef | ||
|
|
f36ddce527 | ||
|
|
70f2c64a5c | ||
|
|
2d61fc6308 | ||
|
|
dd23d81a40 | ||
|
|
a758a770f1 | ||
|
|
1b9711ea2f | ||
|
|
f6eec6d6b7 | ||
|
|
801ccb98ca | ||
|
|
c8323822e8 | ||
|
|
7dfb19d9c3 | ||
|
|
934926e596 | ||
|
|
f06b9146be | ||
|
|
b4b04ec0f9 | ||
|
|
e507e46ced | ||
|
|
47ca2026d0 | ||
|
|
7905a5e71f | ||
|
|
b22d0a69df | ||
|
|
d467e5da5e | ||
|
|
f3b5fe871a | ||
|
|
2a5ce1f78e | ||
|
|
64c5f76ccd | ||
|
|
f350f2d9f3 | ||
|
|
a938ab1d73 | ||
|
|
09829162e5 | ||
|
|
3b6f9f31bb | ||
|
|
07e1e0e53e | ||
|
|
41f6ae0969 | ||
|
|
76ab55a78c |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
node_modules
|
||||
.vscode
|
||||
.vscode
|
||||
.idea
|
||||
661
LICENSE
Normal file
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
147
README.md
Normal file
147
README.md
Normal file
@@ -0,0 +1,147 @@
|
||||
# GB28181插件
|
||||
|
||||
该插件提供SIP server的服务,以及流媒体服务器能力,可以将NVR和摄像头的流抓到m7s中,可获取的设备的录像数据以及访问录像视频。也可以控制摄像头的旋转、缩放等。
|
||||
|
||||
## 插件地址
|
||||
|
||||
github.com/Monibuca/plugin-gb28181
|
||||
|
||||
## 插件引入
|
||||
|
||||
```go
|
||||
import (
|
||||
_ "m7s.live/plugin/gb28181/v4"
|
||||
)
|
||||
```
|
||||
|
||||
## 默认插件配置
|
||||
|
||||
```yaml
|
||||
gb28181:
|
||||
invitemode: 1 #0、手动invite 1、表示自动发起invite,当Server(SIP)接收到设备信息时,立即向设备发送invite命令获取流,2、按需拉流,既等待订阅者触发
|
||||
position:
|
||||
autosubposition: false #是否自动订阅定位
|
||||
expires: 3600s #订阅周期(单位:秒),默认3600
|
||||
interval: 6s #订阅间隔(单位:秒),默认6
|
||||
udpcachesize: 0 #表示UDP缓存大小,默认为0,不开启。仅当TCP关闭,切缓存大于0时才开启
|
||||
sipip: "" #sip服务器地址 默认 自动适配设备网段
|
||||
serial: "34020000002000000001"
|
||||
realm: "3402000000"
|
||||
username: ""
|
||||
password: ""
|
||||
|
||||
registervalidity: 60s #注册有效期
|
||||
|
||||
mediaip: "" #媒体服务器地址 默认 自动适配设备网段
|
||||
port:
|
||||
sip: udp:5060 #sip服务器端口
|
||||
media: tcp:58200-59200 #媒体服务器端口,用于接收设备的流
|
||||
|
||||
removebaninterval: 10m #定时移除注册失败的设备黑名单,单位秒,默认10分钟(600秒)
|
||||
loglevel: info
|
||||
```
|
||||
|
||||
**如果配置了端口范围,将采用范围端口机制,每一个流对应一个端口
|
||||
|
||||
**注意某些摄像机没有设置用户名的地方,摄像机会以自身的国标id作为用户名,这个时候m7s会忽略使用摄像机的用户名,忽略配置的用户名**
|
||||
如果设备配置了错误的用户名和密码,连续三次上报错误后,m7s会记录设备id,并在10分钟内禁止设备注册
|
||||
|
||||
## 插件功能
|
||||
|
||||
### 使用SIP协议接受NVR或其他GB28181设备的注册
|
||||
|
||||
- 服务器启动时自动监听SIP协议端口,当有设备注册时,会记录该设备信息,可以从UI的列表中看到设备
|
||||
- 定时发送Catalog命令查询设备的目录信息,可获得通道数据或者子设备
|
||||
- 发送RecordInfo命令查询设备对录像数据
|
||||
- 发送Invite命令获取设备的实时视频或者录像视频
|
||||
- 发送PTZ命令来控制摄像头云台
|
||||
- 自动同步设备位置
|
||||
|
||||
### 作为GB28281的流媒体服务器接受设备的媒体流
|
||||
|
||||
- 当invite设备的**实时**视频流时,会在m7s中创建对应的流,StreamPath由设备编号和通道编号组成,即[设备编号]/[通道编号],如果有多个层级,通道编号是最后一个层级的编号
|
||||
- 当invite设备的**录像**视频流时,StreamPath由设备编号和通道编号以及录像的起止时间拼接而成即[设备编号]/[通道编号]/[开始时间]-[结束时间]
|
||||
|
||||
### 如何设置UDP缓存大小
|
||||
|
||||
通过wireshark抓包,分析rtp,然后看一下大概多少个包可以有序
|
||||
|
||||
## 接口API
|
||||
|
||||
### 罗列所有的gb28181协议的设备
|
||||
|
||||
`/gb28181/api/list`
|
||||
设备的结构体如下
|
||||
|
||||
```go
|
||||
type Device struct {
|
||||
ID string
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
RegisterTime time.Time
|
||||
UpdateTime time.Time
|
||||
LastKeepaliveAt time.Time
|
||||
Status string
|
||||
Channels []*Channel
|
||||
NetAddr string
|
||||
}
|
||||
```
|
||||
|
||||
> 根据golang的规则,小写字母开头的变量不会被序列化
|
||||
|
||||
### 从设备拉取视频流
|
||||
|
||||
`/gb28181/api/invite`
|
||||
|
||||
| 参数名 | 必传 | 含义 |
|
||||
| --------- | ---- | ---------------------------- |
|
||||
| id | 是 | 设备ID |
|
||||
| channel | 是 | 通道编号 |
|
||||
| startTime | 否 | 开始时间(纯数字Unix时间戳) |
|
||||
| endTime | 否 | 结束时间(纯数字Unix时间戳) |
|
||||
|
||||
返回200代表成功, 304代表已经在拉取中,不能重复拉(仅仅针对直播流)
|
||||
|
||||
### 停止从设备拉流
|
||||
|
||||
`/gb28181/api/bye`
|
||||
|
||||
| 参数名 | 必传 | 含义 |
|
||||
| ------- | ---- | -------- |
|
||||
| id | 是 | 设备ID |
|
||||
| channel | 是 | 通道编号 |
|
||||
|
||||
http 200 表示成功,404流不存在
|
||||
|
||||
### 发送控制命令
|
||||
|
||||
`/gb28181/api/control`
|
||||
|
||||
| 参数名 | 必传 | 含义 |
|
||||
| ------- | ---- | ----------- |
|
||||
| id | 是 | 设备ID |
|
||||
| channel | 是 | 通道编号 |
|
||||
| ptzcmd | 是 | PTZ控制指令 |
|
||||
|
||||
### 查询录像
|
||||
|
||||
`/gb28181/api/records`
|
||||
|
||||
| 参数名 | 必传 | 含义 |
|
||||
| --------- | ---- | -------------------------------------------- |
|
||||
| id | 是 | 设备ID |
|
||||
| channel | 是 | 通道编号 |
|
||||
| startTime | 否 | 开始时间(Unix时间戳) |
|
||||
| endTime | 否 | 结束时间(Unix时间戳) |
|
||||
|
||||
### 移动位置订阅
|
||||
|
||||
`/gb28181/api/position`
|
||||
|
||||
| 参数名 | 必传 | 含义 |
|
||||
| -------- | ---- | -------------- |
|
||||
| id | 是 | 设备ID |
|
||||
| expires | 是 | 订阅周期(秒) |
|
||||
| interval | 是 | 订阅间隔(秒) |
|
||||
537
channel.go
Normal file
537
channel.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/ghettovoice/gosip/sip"
|
||||
"go.uber.org/zap"
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/log"
|
||||
"m7s.live/plugin/gb28181/v4/utils"
|
||||
"m7s.live/plugin/ps/v4"
|
||||
)
|
||||
|
||||
var QUERY_RECORD_TIMEOUT = time.Second * 5
|
||||
|
||||
type PullStream struct {
|
||||
opt *InviteOptions
|
||||
channel *Channel
|
||||
inviteRes sip.Response
|
||||
}
|
||||
|
||||
func (p *PullStream) CreateRequest(method sip.RequestMethod) (req sip.Request) {
|
||||
res := p.inviteRes
|
||||
req = p.channel.CreateRequst(method)
|
||||
from, _ := res.From()
|
||||
to, _ := res.To()
|
||||
callId, _ := res.CallID()
|
||||
req.ReplaceHeaders(from.Name(), []sip.Header{from})
|
||||
req.ReplaceHeaders(to.Name(), []sip.Header{to})
|
||||
req.ReplaceHeaders(callId.Name(), []sip.Header{callId})
|
||||
return
|
||||
}
|
||||
|
||||
func (p *PullStream) Bye() int {
|
||||
req := p.CreateRequest(sip.BYE)
|
||||
resp, err := p.channel.device.SipRequestForResponse(req)
|
||||
if p.opt.IsLive() {
|
||||
p.channel.status.Store(0)
|
||||
}
|
||||
if p.opt.recyclePort != nil {
|
||||
p.opt.recyclePort(p.opt.MediaPort)
|
||||
}
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
return int(resp.StatusCode())
|
||||
}
|
||||
|
||||
func (p *PullStream) info(body string) int {
|
||||
d := p.channel.device
|
||||
req := p.CreateRequest(sip.INFO)
|
||||
contentType := sip.ContentType("Application/MANSRTSP")
|
||||
req.AppendHeader(&contentType)
|
||||
req.SetBody(body, true)
|
||||
|
||||
resp, err := d.SipRequestForResponse(req)
|
||||
if err != nil {
|
||||
log.Warnf("Send info to stream error: %v, stream=%s, body=%s", err, p.opt.StreamPath, body)
|
||||
return getSipRespErrorCode(err)
|
||||
}
|
||||
return int(resp.StatusCode())
|
||||
}
|
||||
|
||||
// 暂停播放
|
||||
func (p *PullStream) Pause() int {
|
||||
body := fmt.Sprintf(`PAUSE RTSP/1.0
|
||||
CSeq: %d
|
||||
PauseTime: now
|
||||
`, p.channel.device.sn)
|
||||
return p.info(body)
|
||||
}
|
||||
|
||||
// 恢复播放
|
||||
func (p *PullStream) Resume() int {
|
||||
d := p.channel.device
|
||||
body := fmt.Sprintf(`PLAY RTSP/1.0
|
||||
CSeq: %d
|
||||
Range: npt=now-
|
||||
`, d.sn)
|
||||
return p.info(body)
|
||||
}
|
||||
|
||||
// 跳转到播放时间
|
||||
// second: 相对于起始点调整到第 sec 秒播放
|
||||
func (p *PullStream) PlayAt(second uint) int {
|
||||
d := p.channel.device
|
||||
body := fmt.Sprintf(`PLAY RTSP/1.0
|
||||
CSeq: %d
|
||||
Range: npt=%d-
|
||||
`, d.sn, second)
|
||||
return p.info(body)
|
||||
}
|
||||
|
||||
// 快进/快退播放
|
||||
// speed 取值: 0.25 0.5 1 2 4 或者其对应的负数表示倒放
|
||||
func (p *PullStream) PlayForward(speed float32) int {
|
||||
d := p.channel.device
|
||||
body := fmt.Sprintf(`PLAY RTSP/1.0
|
||||
CSeq: %d
|
||||
Scale: %0.6f
|
||||
`, d.sn, speed)
|
||||
return p.info(body)
|
||||
}
|
||||
|
||||
type Channel struct {
|
||||
device *Device // 所属设备
|
||||
status atomic.Int32 // 通道状态,0:空闲,1:正在invite,2:正在播放
|
||||
LiveSubSP string // 实时子码流,通过rtsp
|
||||
GpsTime time.Time //gps时间
|
||||
Longitude string //经度
|
||||
Latitude string //纬度
|
||||
*log.Logger `json:"-" yaml:"-"`
|
||||
ChannelInfo
|
||||
}
|
||||
|
||||
// Channel 通道
|
||||
type ChannelInfo struct {
|
||||
DeviceID string // 通道ID
|
||||
ParentID string
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
CivilCode string
|
||||
Address string
|
||||
Port int
|
||||
Parental int
|
||||
SafetyWay int
|
||||
RegisterWay int
|
||||
Secrecy int
|
||||
Status ChannelStatus
|
||||
}
|
||||
|
||||
type ChannelStatus string
|
||||
|
||||
const (
|
||||
ChannelOnStatus = "ON"
|
||||
ChannelOffStatus = "OFF"
|
||||
)
|
||||
|
||||
func (channel *Channel) CreateRequst(Method sip.RequestMethod) (req sip.Request) {
|
||||
d := channel.device
|
||||
d.sn++
|
||||
|
||||
callId := sip.CallID(utils.RandNumString(10))
|
||||
userAgent := sip.UserAgentHeader("Monibuca")
|
||||
maxForwards := sip.MaxForwards(70) //增加max-forwards为默认值 70
|
||||
cseq := sip.CSeq{
|
||||
SeqNo: uint32(d.sn),
|
||||
MethodName: Method,
|
||||
}
|
||||
port := sip.Port(conf.SipPort)
|
||||
serverAddr := sip.Address{
|
||||
//DisplayName: sip.String{Str: d.serverConfig.Serial},
|
||||
Uri: &sip.SipUri{
|
||||
FUser: sip.String{Str: conf.Serial},
|
||||
FHost: d.sipIP,
|
||||
FPort: &port,
|
||||
},
|
||||
Params: sip.NewParams().Add("tag", sip.String{Str: utils.RandNumString(9)}),
|
||||
}
|
||||
//非同一域的目标地址需要使用@host
|
||||
host := conf.Realm
|
||||
if channel.DeviceID[0:9] != host {
|
||||
if channel.Port != 0 {
|
||||
deviceIp := d.NetAddr
|
||||
deviceIp = deviceIp[0:strings.LastIndex(deviceIp, ":")]
|
||||
host = fmt.Sprintf("%s:%d", deviceIp, channel.Port)
|
||||
} else {
|
||||
host = d.NetAddr
|
||||
}
|
||||
}
|
||||
|
||||
channelAddr := sip.Address{
|
||||
//DisplayName: sip.String{Str: d.serverConfig.Serial},
|
||||
Uri: &sip.SipUri{FUser: sip.String{Str: channel.DeviceID}, FHost: host},
|
||||
}
|
||||
req = sip.NewRequest(
|
||||
"",
|
||||
Method,
|
||||
channelAddr.Uri,
|
||||
"SIP/2.0",
|
||||
[]sip.Header{
|
||||
serverAddr.AsFromHeader(),
|
||||
channelAddr.AsToHeader(),
|
||||
&callId,
|
||||
&userAgent,
|
||||
&cseq,
|
||||
&maxForwards,
|
||||
serverAddr.AsContactHeader(),
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
|
||||
req.SetTransport(conf.SipNetwork)
|
||||
req.SetDestination(d.NetAddr)
|
||||
return req
|
||||
}
|
||||
|
||||
func (channel *Channel) QueryRecord(startTime, endTime string) ([]*Record, error) {
|
||||
d := channel.device
|
||||
request := d.CreateRequest(sip.MESSAGE)
|
||||
contentType := sip.ContentType("Application/MANSCDP+xml")
|
||||
request.AppendHeader(&contentType)
|
||||
// body := fmt.Sprintf(`<?xml version="1.0"?>
|
||||
// <Query>
|
||||
// <CmdType>RecordInfo</CmdType>
|
||||
// <SN>%d</SN>
|
||||
// <DeviceID>%s</DeviceID>
|
||||
// <StartTime>%s</StartTime>
|
||||
// <EndTime>%s</EndTime>
|
||||
// <Secrecy>0</Secrecy>
|
||||
// <Type>all</Type>
|
||||
// </Query>`, d.sn, channel.DeviceID, startTime, endTime)
|
||||
start, _ := strconv.ParseInt(startTime, 10, 0)
|
||||
end, _ := strconv.ParseInt(endTime, 10, 0)
|
||||
body := BuildRecordInfoXML(d.sn, channel.DeviceID, start, end)
|
||||
request.SetBody(body, true)
|
||||
|
||||
resultCh := RecordQueryLink.WaitResult(d.ID, channel.DeviceID, d.sn, QUERY_RECORD_TIMEOUT)
|
||||
resp, err := d.SipRequestForResponse(request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("query error: %s", err)
|
||||
}
|
||||
if resp.StatusCode() != http.StatusOK {
|
||||
return nil, fmt.Errorf("query error, status=%d", resp.StatusCode())
|
||||
}
|
||||
// RecordQueryLink 中加了超时机制,该结果一定会返回
|
||||
// 所以此处不用再增加超时等保护机制
|
||||
r := <-resultCh
|
||||
return r.list, r.err
|
||||
}
|
||||
|
||||
func (channel *Channel) Control(PTZCmd string) int {
|
||||
d := channel.device
|
||||
request := d.CreateRequest(sip.MESSAGE)
|
||||
contentType := sip.ContentType("Application/MANSCDP+xml")
|
||||
request.AppendHeader(&contentType)
|
||||
body := fmt.Sprintf(`<?xml version="1.0"?>
|
||||
<Control>
|
||||
<CmdType>DeviceControl</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
<PTZCmd>%s</PTZCmd>
|
||||
</Control>`, d.sn, channel.DeviceID, PTZCmd)
|
||||
request.SetBody(body, true)
|
||||
resp, err := d.SipRequestForResponse(request)
|
||||
if err != nil {
|
||||
return http.StatusRequestTimeout
|
||||
}
|
||||
return int(resp.StatusCode())
|
||||
}
|
||||
|
||||
// Invite 发送Invite报文 invites a channel to play
|
||||
// 注意里面的锁保证不同时发送invite报文,该锁由channel持有
|
||||
/***
|
||||
f字段: f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
|
||||
各项具体含义:
|
||||
v:后续参数为视频的参数;各参数间以 “/”分割;
|
||||
编码格式:十进制整数字符串表示
|
||||
1 –MPEG-4 2 –H.264 3 – SVAC 4 –3GP
|
||||
分辨率:十进制整数字符串表示
|
||||
1 – QCIF 2 – CIF 3 – 4CIF 4 – D1 5 –720P 6 –1080P/I
|
||||
帧率:十进制整数字符串表示 0~99
|
||||
码率类型:十进制整数字符串表示
|
||||
1 – 固定码率(CBR) 2 – 可变码率(VBR)
|
||||
码率大小:十进制整数字符串表示 0~100000(如 1表示1kbps)
|
||||
a:后续参数为音频的参数;各参数间以 “/”分割;
|
||||
编码格式:十进制整数字符串表示
|
||||
1 – G.711 2 – G.723.1 3 – G.729 4 – G.722.1
|
||||
码率大小:十进制整数字符串
|
||||
音频编码码率: 1 — 5.3 kbps (注:G.723.1中使用)
|
||||
2 — 6.3 kbps (注:G.723.1中使用)
|
||||
3 — 8 kbps (注:G.729中使用)
|
||||
4 — 16 kbps (注:G.722.1中使用)
|
||||
5 — 24 kbps (注:G.722.1中使用)
|
||||
6 — 32 kbps (注:G.722.1中使用)
|
||||
7 — 48 kbps (注:G.722.1中使用)
|
||||
8 — 64 kbps(注:G.711中使用)
|
||||
采样率:十进制整数字符串表示
|
||||
1 — 8 kHz(注:G.711/ G.723.1/ G.729中使用)
|
||||
2—14 kHz(注:G.722.1中使用)
|
||||
3—16 kHz(注:G.722.1中使用)
|
||||
4—32 kHz(注:G.722.1中使用)
|
||||
注1:字符串说明
|
||||
本节中使用的“十进制整数字符串”的含义为“0”~“4294967296” 之间的十进制数字字符串。
|
||||
注2:参数分割标识
|
||||
各参数间以“/”分割,参数间的分割符“/”不能省略;
|
||||
若两个分割符 “/”间的某参数为空时(即两个分割符 “/”直接将相连时)表示无该参数值;
|
||||
注3:f字段说明
|
||||
使用f字段时,应保证视频和音频参数的结构完整性,即在任何时候,f字段的结构都应是完整的结构:
|
||||
f = v/编码格式/分辨率/帧率/码率类型/码率大小a/编码格式/码率大小/采样率
|
||||
若只有视频时,音频中的各参数项可以不填写,但应保持 “a///”的结构:
|
||||
f = v/编码格式/分辨率/帧率/码率类型/码率大小a///
|
||||
若只有音频时也类似处理,视频中的各参数项可以不填写,但应保持 “v/”的结构:
|
||||
f = v/a/编码格式/码率大小/采样率
|
||||
f字段中视、音频参数段之间不需空格分割。
|
||||
可使用f字段中的分辨率参数标识同一设备不同分辨率的码流。
|
||||
*/
|
||||
|
||||
func (channel *Channel) Invite(opt *InviteOptions) (code int, err error) {
|
||||
if opt.IsLive() {
|
||||
if !channel.status.CompareAndSwap(0, 1) {
|
||||
return 304, nil
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
GB28181Plugin.Error("Invite", zap.Error(err))
|
||||
channel.status.Store(0)
|
||||
if conf.InviteMode == 1 {
|
||||
// 5秒后重试
|
||||
time.AfterFunc(time.Second*5, func() {
|
||||
channel.Invite(opt)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
channel.status.Store(2)
|
||||
}
|
||||
}()
|
||||
}
|
||||
d := channel.device
|
||||
streamPath := fmt.Sprintf("%s/%s", d.ID, channel.DeviceID)
|
||||
s := "Play"
|
||||
opt.CreateSSRC()
|
||||
if opt.Record() {
|
||||
s = "Playback"
|
||||
streamPath = fmt.Sprintf("%s/%s/%d-%d", d.ID, channel.DeviceID, opt.Start, opt.End)
|
||||
}
|
||||
if opt.StreamPath != "" {
|
||||
streamPath = opt.StreamPath
|
||||
} else {
|
||||
opt.StreamPath = streamPath
|
||||
}
|
||||
if opt.dump == "" {
|
||||
opt.dump = conf.DumpPath
|
||||
}
|
||||
protocol := ""
|
||||
networkType := "udp"
|
||||
reusePort := true
|
||||
if conf.IsMediaNetworkTCP() {
|
||||
networkType = "tcp"
|
||||
protocol = "TCP/"
|
||||
if conf.tcpPorts.Valid {
|
||||
opt.MediaPort, err = conf.tcpPorts.GetPort()
|
||||
opt.recyclePort = conf.tcpPorts.Recycle
|
||||
reusePort = false
|
||||
}
|
||||
} else {
|
||||
if conf.udpPorts.Valid {
|
||||
opt.MediaPort, err = conf.udpPorts.GetPort()
|
||||
opt.recyclePort = conf.udpPorts.Recycle
|
||||
reusePort = false
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
if opt.MediaPort == 0 {
|
||||
opt.MediaPort = conf.MediaPort
|
||||
}
|
||||
|
||||
sdpInfo := []string{
|
||||
"v=0",
|
||||
fmt.Sprintf("o=%s 0 0 IN IP4 %s", channel.DeviceID, d.mediaIP),
|
||||
"s=" + s,
|
||||
"u=" + channel.DeviceID + ":0",
|
||||
"c=IN IP4 " + d.mediaIP,
|
||||
opt.String(),
|
||||
fmt.Sprintf("m=video %d %sRTP/AVP 96", opt.MediaPort, protocol),
|
||||
"a=recvonly",
|
||||
"a=rtpmap:96 PS/90000",
|
||||
"y=" + opt.ssrc,
|
||||
}
|
||||
if conf.IsMediaNetworkTCP() {
|
||||
sdpInfo = append(sdpInfo, "a=setup:passive", "a=connection:new")
|
||||
}
|
||||
invite := channel.CreateRequst(sip.INVITE)
|
||||
contentType := sip.ContentType("application/sdp")
|
||||
invite.AppendHeader(&contentType)
|
||||
|
||||
invite.SetBody(strings.Join(sdpInfo, "\r\n")+"\r\n", true)
|
||||
|
||||
subject := sip.GenericHeader{
|
||||
HeaderName: "Subject", Contents: fmt.Sprintf("%s:%s,%s:0", channel.DeviceID, opt.ssrc, conf.Serial),
|
||||
}
|
||||
invite.AppendHeader(&subject)
|
||||
inviteRes, err := d.SipRequestForResponse(invite)
|
||||
if err != nil {
|
||||
channel.Error("invite", zap.Error(err), zap.String("msg", invite.String()))
|
||||
return http.StatusInternalServerError, err
|
||||
}
|
||||
code = int(inviteRes.StatusCode())
|
||||
channel.Info("invite response", zap.Int("status code", code))
|
||||
|
||||
if code == http.StatusOK {
|
||||
ds := strings.Split(inviteRes.Body(), "\r\n")
|
||||
for _, l := range ds {
|
||||
if ls := strings.Split(l, "="); len(ls) > 1 {
|
||||
if ls[0] == "y" && len(ls[1]) > 0 {
|
||||
if _ssrc, err := strconv.ParseInt(ls[1], 10, 0); err == nil {
|
||||
opt.SSRC = uint32(_ssrc)
|
||||
} else {
|
||||
channel.Error("read invite response y ", zap.Error(err))
|
||||
}
|
||||
// break
|
||||
}
|
||||
if ls[0] == "m" && len(ls[1]) > 0 {
|
||||
netinfo := strings.Split(ls[1], " ")
|
||||
if strings.ToUpper(netinfo[2]) == "TCP/RTP/AVP" {
|
||||
channel.Debug("Device support tcp")
|
||||
} else {
|
||||
channel.Debug("Device not support tcp")
|
||||
networkType = "udp"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
err = ps.Receive(streamPath, opt.dump, fmt.Sprintf("%s:%d", networkType, opt.MediaPort), opt.SSRC, reusePort)
|
||||
if err == nil {
|
||||
PullStreams.Store(streamPath, &PullStream{
|
||||
opt: opt,
|
||||
channel: channel,
|
||||
inviteRes: inviteRes,
|
||||
})
|
||||
err = srv.Send(sip.NewAckRequest("", invite, inviteRes, "", nil))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (channel *Channel) Bye(streamPath string) int {
|
||||
d := channel.device
|
||||
if streamPath == "" {
|
||||
streamPath = fmt.Sprintf("%s/%s", d.ID, channel.DeviceID)
|
||||
}
|
||||
if s, loaded := PullStreams.LoadAndDelete(streamPath); loaded {
|
||||
s.(*PullStream).Bye()
|
||||
if s := Streams.Get(streamPath); s != nil {
|
||||
s.Close()
|
||||
}
|
||||
return http.StatusOK
|
||||
}
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
func (channel *Channel) Pause(streamPath string) int {
|
||||
if s, loaded := PullStreams.Load(streamPath); loaded {
|
||||
r := s.(*PullStream).Pause()
|
||||
if s := Streams.Get(streamPath); s != nil {
|
||||
s.Pause()
|
||||
}
|
||||
return r
|
||||
}
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
func (channel *Channel) Resume(streamPath string) int {
|
||||
if s, loaded := PullStreams.Load(streamPath); loaded {
|
||||
r := s.(*PullStream).Resume()
|
||||
if s := Streams.Get(streamPath); s != nil {
|
||||
s.Resume()
|
||||
}
|
||||
return r
|
||||
}
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
func (channel *Channel) PlayAt(streamPath string, second uint) int {
|
||||
if s, loaded := PullStreams.Load(streamPath); loaded {
|
||||
r := s.(*PullStream).PlayAt(second)
|
||||
if s := Streams.Get(streamPath); s != nil {
|
||||
s.Resume()
|
||||
}
|
||||
return r
|
||||
}
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
func (channel *Channel) PlayForward(streamPath string, speed float32) int {
|
||||
if s, loaded := PullStreams.Load(streamPath); loaded {
|
||||
return s.(*PullStream).PlayForward(speed)
|
||||
}
|
||||
if s := Streams.Get(streamPath); s != nil {
|
||||
s.Resume()
|
||||
}
|
||||
return http.StatusNotFound
|
||||
}
|
||||
|
||||
func (channel *Channel) TryAutoInvite(opt *InviteOptions) {
|
||||
if channel.CanInvite() {
|
||||
go channel.Invite(opt)
|
||||
}
|
||||
}
|
||||
|
||||
func (channel *Channel) CanInvite() bool {
|
||||
if channel.status.Load() != 0 || len(channel.DeviceID) != 20 || channel.Status == ChannelOffStatus {
|
||||
return false
|
||||
}
|
||||
|
||||
if conf.InviteIDs == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 11~13位是设备类型编码
|
||||
typeID := channel.DeviceID[10:13]
|
||||
|
||||
// format: start-end,type1,type2
|
||||
tokens := strings.Split(conf.InviteIDs, ",")
|
||||
for _, tok := range tokens {
|
||||
if first, second, ok := strings.Cut(tok, "-"); ok {
|
||||
if typeID >= first && typeID <= second {
|
||||
return true
|
||||
}
|
||||
} else {
|
||||
if typeID == first {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getSipRespErrorCode(err error) int {
|
||||
if re, ok := err.(*sip.RequestError); ok {
|
||||
return int(re.Code)
|
||||
} else {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,18 @@
|
||||
package sip
|
||||
package gb28181
|
||||
|
||||
import "fmt"
|
||||
|
||||
//transaction sip error
|
||||
var errorMap = map[int]string{
|
||||
//1xx
|
||||
var reasons = map[int]string{
|
||||
100: "Trying",
|
||||
180: "Ringing",
|
||||
181: "Call Is Being Forwarded",
|
||||
182: "Queued",
|
||||
183: "Session Progress",
|
||||
199: "Early Dialog Terminated",
|
||||
//2xx
|
||||
200: "OK",
|
||||
202: "Accepted",
|
||||
204: "No Notification",
|
||||
//3xx
|
||||
300: "Multiple Choices",
|
||||
301: "Moved Permanently",
|
||||
302: "Moved Temporarily",
|
||||
305: "Use Proxy",
|
||||
380: "Alternative Service",
|
||||
//4xx
|
||||
400: "Bad Request",
|
||||
401: "Unauthorized",
|
||||
402: "Payment Required",
|
||||
@@ -31,33 +22,16 @@ var errorMap = map[int]string{
|
||||
406: "Not Acceptable",
|
||||
407: "Proxy Authentication Required",
|
||||
408: "Request Timeout",
|
||||
409: "Conflict",
|
||||
410: "Gone",
|
||||
411: "Length Required",
|
||||
412: "Conditional Request Failed",
|
||||
413: "Request Entity Too Large",
|
||||
414: "Request-URI Too Long",
|
||||
415: "Unsupported Media Type",
|
||||
416: "Unsupported URI Scheme",
|
||||
417: "Unknown Resource-Priority",
|
||||
420: "Bad Extension",
|
||||
421: "Extension Required",
|
||||
422: "Session Interval Too Small",
|
||||
423: "Interval Too Brief",
|
||||
424: "Bad Location Information",
|
||||
428: "Use Identity Header",
|
||||
429: "Provide Referrer Identity",
|
||||
430: "Flow Failed",
|
||||
433: "Anonymity Disallowed",
|
||||
436: "Bad Identity Info",
|
||||
437: "Unsupported Credential",
|
||||
438: "Invalid Identity Header",
|
||||
439: "First Hop Lacks Outbound Support",
|
||||
440: "Max-Breadth Exceeded",
|
||||
469: "Bad Info Package",
|
||||
470: "Consent Needed",
|
||||
480: "Temporarily Unavailable",
|
||||
481: "Call/Transaction Does Not Exist",
|
||||
481: "Call transaction Does Not Exist",
|
||||
482: "Loop Detected",
|
||||
483: "Too Many Hops",
|
||||
484: "Address Incomplete",
|
||||
@@ -68,29 +42,25 @@ var errorMap = map[int]string{
|
||||
489: "Bad Event",
|
||||
491: "Request Pending",
|
||||
493: "Undecipherable",
|
||||
494: "Security Agreement Required",
|
||||
//5xx
|
||||
500: "Server Internal Error",
|
||||
501: "Not Implemented",
|
||||
502: "Bad Gateway",
|
||||
503: "Service Unavailable",
|
||||
504: "Server Time-out",
|
||||
504: "Server Tim",
|
||||
505: "Version Not Supported",
|
||||
513: "Message Too Large",
|
||||
580: "Precondition Failure",
|
||||
//6xx
|
||||
513: "message Too Large",
|
||||
600: "Busy Everywhere",
|
||||
603: "Decline",
|
||||
604: "Does Not Exist Anywhere",
|
||||
606: "Not Acceptable",
|
||||
607: "Unwanted",
|
||||
687: "Dialog Terminated",
|
||||
606: "SESSION NOT ACCEPTABLE",
|
||||
}
|
||||
|
||||
func DumpError(code int) string {
|
||||
if code == 0{
|
||||
return "invalid status reason for request"
|
||||
}
|
||||
return fmt.Sprintf("%d %s", code, errorMap[code])
|
||||
func Explain(statusCode int) string {
|
||||
return reasons[statusCode]
|
||||
}
|
||||
|
||||
const (
|
||||
INVIDE_MODE_MANUAL = iota
|
||||
INVIDE_MODE_AUTO
|
||||
INVIDE_MODE_ONSUBSCRIBE
|
||||
)
|
||||
543
device.go
Normal file
543
device.go
Normal file
@@ -0,0 +1,543 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/log"
|
||||
"m7s.live/plugin/gb28181/v4/utils"
|
||||
|
||||
// . "github.com/logrusorgru/aurora"
|
||||
"github.com/ghettovoice/gosip/sip"
|
||||
myip "github.com/husanpao/ip"
|
||||
)
|
||||
|
||||
const TIME_LAYOUT = "2006-01-02T15:04:05"
|
||||
|
||||
// Record 录像
|
||||
type Record struct {
|
||||
DeviceID string
|
||||
Name string
|
||||
FilePath string
|
||||
Address string
|
||||
StartTime string
|
||||
EndTime string
|
||||
Secrecy int
|
||||
Type string
|
||||
}
|
||||
|
||||
func (r *Record) GetPublishStreamPath() string {
|
||||
return fmt.Sprintf("%s/%s", r.DeviceID, r.StartTime)
|
||||
}
|
||||
|
||||
var (
|
||||
Devices sync.Map
|
||||
DeviceNonce sync.Map //保存nonce防止设备伪造
|
||||
DeviceRegisterCount sync.Map //设备注册次数
|
||||
)
|
||||
|
||||
type DeviceStatus string
|
||||
|
||||
const (
|
||||
DeviceRegisterStatus = "REGISTER"
|
||||
DeviceRecoverStatus = "RECOVER"
|
||||
DeviceOnlineStatus = "ONLINE"
|
||||
DeviceOfflineStatus = "OFFLINE"
|
||||
DeviceAlarmedStatus = "ALARMED"
|
||||
)
|
||||
|
||||
type Device struct {
|
||||
//*transaction.Core `json:"-" yaml:"-"`
|
||||
ID string
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
RegisterTime time.Time
|
||||
UpdateTime time.Time
|
||||
LastKeepaliveAt time.Time
|
||||
Status DeviceStatus
|
||||
sn int
|
||||
addr sip.Address
|
||||
sipIP string //设备对应网卡的服务器ip
|
||||
mediaIP string //设备对应网卡的服务器ip
|
||||
NetAddr string
|
||||
channelMap sync.Map
|
||||
subscriber struct {
|
||||
CallID string
|
||||
Timeout time.Time
|
||||
}
|
||||
lastSyncTime time.Time
|
||||
GpsTime time.Time //gps时间
|
||||
Longitude string //经度
|
||||
Latitude string //纬度
|
||||
*log.Logger `json:"-" yaml:"-"`
|
||||
}
|
||||
|
||||
func (d *Device) MarshalJSON() ([]byte, error) {
|
||||
type Alias Device
|
||||
data := &struct {
|
||||
Channels []*ChannelInfo
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(d),
|
||||
}
|
||||
d.channelMap.Range(func(key, value interface{}) bool {
|
||||
c := value.(*Channel)
|
||||
data.Channels = append(data.Channels, &c.ChannelInfo)
|
||||
return true
|
||||
})
|
||||
return json.Marshal(data)
|
||||
}
|
||||
func (c *GB28181Config) RecoverDevice(d *Device, req sip.Request) {
|
||||
from, _ := req.From()
|
||||
d.addr = sip.Address{
|
||||
DisplayName: from.DisplayName,
|
||||
Uri: from.Address,
|
||||
}
|
||||
deviceIp := req.Source()
|
||||
servIp := req.Recipient().Host()
|
||||
//根据网卡ip获取对应的公网ip
|
||||
sipIP := c.routes[servIp]
|
||||
//如果相等,则服务器是内网通道.海康摄像头不支持...自动获取
|
||||
if strings.LastIndex(deviceIp, ".") != -1 && strings.LastIndex(servIp, ".") != -1 {
|
||||
if servIp[0:strings.LastIndex(servIp, ".")] == deviceIp[0:strings.LastIndex(deviceIp, ".")] || sipIP == "" {
|
||||
sipIP = servIp
|
||||
}
|
||||
}
|
||||
//如果用户配置过则使用配置的
|
||||
if c.SipIP != "" {
|
||||
sipIP = c.SipIP
|
||||
} else if sipIP == "" {
|
||||
sipIP = myip.InternalIPv4()
|
||||
}
|
||||
mediaIp := sipIP
|
||||
if c.MediaIP != "" {
|
||||
mediaIp = c.MediaIP
|
||||
}
|
||||
d.Info("RecoverDevice", zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
|
||||
d.Status = DeviceRegisterStatus
|
||||
d.sipIP = sipIP
|
||||
d.mediaIP = mediaIp
|
||||
d.NetAddr = deviceIp
|
||||
d.UpdateTime = time.Now()
|
||||
}
|
||||
|
||||
func (c *GB28181Config) StoreDevice(id string, req sip.Request) (d *Device) {
|
||||
from, _ := req.From()
|
||||
deviceAddr := sip.Address{
|
||||
DisplayName: from.DisplayName,
|
||||
Uri: from.Address,
|
||||
}
|
||||
deviceIp := req.Source()
|
||||
if _d, loaded := Devices.Load(id); loaded {
|
||||
d = _d.(*Device)
|
||||
d.UpdateTime = time.Now()
|
||||
d.NetAddr = deviceIp
|
||||
d.addr = deviceAddr
|
||||
d.Debug("UpdateDevice", zap.String("netaddr", d.NetAddr))
|
||||
} else {
|
||||
servIp := req.Recipient().Host()
|
||||
//根据网卡ip获取对应的公网ip
|
||||
sipIP := c.routes[servIp]
|
||||
//如果相等,则服务器是内网通道.海康摄像头不支持...自动获取
|
||||
if strings.LastIndex(deviceIp, ".") != -1 && strings.LastIndex(servIp, ".") != -1 {
|
||||
if servIp[0:strings.LastIndex(servIp, ".")] == deviceIp[0:strings.LastIndex(deviceIp, ".")] || sipIP == "" {
|
||||
sipIP = servIp
|
||||
}
|
||||
}
|
||||
//如果用户配置过则使用配置的
|
||||
if c.SipIP != "" {
|
||||
sipIP = c.SipIP
|
||||
} else if sipIP == "" {
|
||||
sipIP = myip.InternalIPv4()
|
||||
}
|
||||
mediaIp := sipIP
|
||||
if c.MediaIP != "" {
|
||||
mediaIp = c.MediaIP
|
||||
}
|
||||
d = &Device{
|
||||
ID: id,
|
||||
RegisterTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
Status: DeviceRegisterStatus,
|
||||
addr: deviceAddr,
|
||||
sipIP: sipIP,
|
||||
mediaIP: mediaIp,
|
||||
NetAddr: deviceIp,
|
||||
Logger: GB28181Plugin.With(zap.String("id", id)),
|
||||
}
|
||||
d.Info("StoreDevice", zap.String("deviceIp", deviceIp), zap.String("servIp", servIp), zap.String("sipIP", sipIP), zap.String("mediaIp", mediaIp))
|
||||
Devices.Store(id, d)
|
||||
c.SaveDevices()
|
||||
}
|
||||
return
|
||||
}
|
||||
func (c *GB28181Config) ReadDevices() {
|
||||
if f, err := os.OpenFile("devices.json", os.O_RDONLY, 0644); err == nil {
|
||||
defer f.Close()
|
||||
var items []*Device
|
||||
if err = json.NewDecoder(f).Decode(&items); err == nil {
|
||||
for _, item := range items {
|
||||
if time.Since(item.UpdateTime) < conf.RegisterValidity {
|
||||
item.Status = "RECOVER"
|
||||
item.Logger = GB28181Plugin.With(zap.String("id", item.ID))
|
||||
Devices.Store(item.ID, item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c *GB28181Config) SaveDevices() {
|
||||
var item []any
|
||||
Devices.Range(func(key, value any) bool {
|
||||
item = append(item, value)
|
||||
return true
|
||||
})
|
||||
if f, err := os.OpenFile("devices.json", os.O_WRONLY|os.O_CREATE, 0644); err == nil {
|
||||
defer f.Close()
|
||||
encoder := json.NewEncoder(f)
|
||||
encoder.SetIndent("", " ")
|
||||
encoder.Encode(item)
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) addOrUpdateChannel(info ChannelInfo) (c *Channel) {
|
||||
if old, ok := d.channelMap.Load(info.DeviceID); ok {
|
||||
c = old.(*Channel)
|
||||
c.ChannelInfo = info
|
||||
} else {
|
||||
c = &Channel{
|
||||
device: d,
|
||||
ChannelInfo: info,
|
||||
Logger: d.Logger.With(zap.String("channel", info.DeviceID)),
|
||||
}
|
||||
if s := engine.Streams.Get(fmt.Sprintf("%s/%s/rtsp", c.device.ID, c.DeviceID)); s != nil {
|
||||
c.LiveSubSP = s.Path
|
||||
} else {
|
||||
c.LiveSubSP = ""
|
||||
}
|
||||
d.channelMap.Store(info.DeviceID, c)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) deleteChannel(DeviceID string) {
|
||||
d.channelMap.Delete(DeviceID)
|
||||
}
|
||||
|
||||
func (d *Device) UpdateChannels(list ...ChannelInfo) {
|
||||
for _, c := range list {
|
||||
if _, ok := conf.Ignores[c.DeviceID]; ok {
|
||||
continue
|
||||
}
|
||||
//当父设备非空且存在时、父设备节点增加通道
|
||||
if c.ParentID != "" {
|
||||
path := strings.Split(c.ParentID, "/")
|
||||
parentId := path[len(path)-1]
|
||||
//如果父ID并非本身所属设备,一般情况下这是因为下级设备上传了目录信息,该信息通常不需要处理。
|
||||
// 暂时不考虑级联目录的实现
|
||||
if d.ID != parentId {
|
||||
if v, ok := Devices.Load(parentId); ok {
|
||||
parent := v.(*Device)
|
||||
parent.addOrUpdateChannel(c)
|
||||
continue
|
||||
} else {
|
||||
c.Model = "Directory " + c.Model
|
||||
c.Status = "NoParent"
|
||||
}
|
||||
}
|
||||
}
|
||||
//本设备增加通道
|
||||
channel := d.addOrUpdateChannel(c)
|
||||
|
||||
if conf.InviteMode == INVIDE_MODE_AUTO {
|
||||
channel.TryAutoInvite(&InviteOptions{})
|
||||
}
|
||||
if s := engine.Streams.Get("sub/" + c.DeviceID); s != nil {
|
||||
channel.LiveSubSP = s.Path
|
||||
} else {
|
||||
channel.LiveSubSP = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) CreateRequest(Method sip.RequestMethod) (req sip.Request) {
|
||||
d.sn++
|
||||
|
||||
callId := sip.CallID(utils.RandNumString(10))
|
||||
userAgent := sip.UserAgentHeader("Monibuca")
|
||||
maxForwards := sip.MaxForwards(70) //增加max-forwards为默认值 70
|
||||
cseq := sip.CSeq{
|
||||
SeqNo: uint32(d.sn),
|
||||
MethodName: Method,
|
||||
}
|
||||
port := sip.Port(conf.SipPort)
|
||||
serverAddr := sip.Address{
|
||||
//DisplayName: sip.String{Str: d.config.Serial},
|
||||
Uri: &sip.SipUri{
|
||||
FUser: sip.String{Str: conf.Serial},
|
||||
FHost: d.sipIP,
|
||||
FPort: &port,
|
||||
},
|
||||
Params: sip.NewParams().Add("tag", sip.String{Str: utils.RandNumString(9)}),
|
||||
}
|
||||
req = sip.NewRequest(
|
||||
"",
|
||||
Method,
|
||||
d.addr.Uri,
|
||||
"SIP/2.0",
|
||||
[]sip.Header{
|
||||
serverAddr.AsFromHeader(),
|
||||
d.addr.AsToHeader(),
|
||||
&callId,
|
||||
&userAgent,
|
||||
&cseq,
|
||||
&maxForwards,
|
||||
serverAddr.AsContactHeader(),
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
|
||||
req.SetTransport(conf.SipNetwork)
|
||||
req.SetDestination(d.NetAddr)
|
||||
//fmt.Printf("构建请求参数:%s", *&req)
|
||||
// requestMsg.DestAdd, err2 = d.ResolveAddress(requestMsg)
|
||||
// if err2 != nil {
|
||||
// return nil
|
||||
// }
|
||||
//intranet ip , let's resolve it with public ip
|
||||
// var deviceIp, deviceSourceIP net.IP
|
||||
// switch addr := requestMsg.DestAdd.(type) {
|
||||
// case *net.UDPAddr:
|
||||
// deviceIp = addr.IP
|
||||
// case *net.TCPAddr:
|
||||
// deviceIp = addr.IP
|
||||
// }
|
||||
|
||||
// switch addr2 := d.SourceAddr.(type) {
|
||||
// case *net.UDPAddr:
|
||||
// deviceSourceIP = addr2.IP
|
||||
// case *net.TCPAddr:
|
||||
// deviceSourceIP = addr2.IP
|
||||
// }
|
||||
// if deviceIp.IsPrivate() && !deviceSourceIP.IsPrivate() {
|
||||
// requestMsg.DestAdd = d.SourceAddr
|
||||
// }
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Device) Subscribe() int {
|
||||
request := d.CreateRequest(sip.SUBSCRIBE)
|
||||
if d.subscriber.CallID != "" {
|
||||
callId := sip.CallID(utils.RandNumString(10))
|
||||
request.AppendHeader(&callId)
|
||||
}
|
||||
expires := sip.Expires(3600)
|
||||
d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))
|
||||
contentType := sip.ContentType("Application/MANSCDP+xml")
|
||||
request.AppendHeader(&contentType)
|
||||
request.AppendHeader(&expires)
|
||||
|
||||
request.SetBody(BuildCatalogXML(d.sn, d.ID), true)
|
||||
|
||||
response, err := d.SipRequestForResponse(request)
|
||||
if err == nil && response != nil {
|
||||
if response.StatusCode() == http.StatusOK {
|
||||
callId, _ := request.CallID()
|
||||
d.subscriber.CallID = string(*callId)
|
||||
} else {
|
||||
d.subscriber.CallID = ""
|
||||
}
|
||||
return int(response.StatusCode())
|
||||
}
|
||||
return http.StatusRequestTimeout
|
||||
}
|
||||
|
||||
func (d *Device) Catalog() int {
|
||||
//os.Stdout.Write(debug.Stack())
|
||||
request := d.CreateRequest(sip.MESSAGE)
|
||||
expires := sip.Expires(3600)
|
||||
d.subscriber.Timeout = time.Now().Add(time.Second * time.Duration(expires))
|
||||
contentType := sip.ContentType("Application/MANSCDP+xml")
|
||||
|
||||
request.AppendHeader(&contentType)
|
||||
request.AppendHeader(&expires)
|
||||
request.SetBody(BuildCatalogXML(d.sn, d.ID), true)
|
||||
// 输出Sip请求设备通道信息信令
|
||||
GB28181Plugin.Sugar().Debugf("SIP->Catalog:%s", request)
|
||||
resp, err := d.SipRequestForResponse(request)
|
||||
if err == nil && resp != nil {
|
||||
GB28181Plugin.Sugar().Debugf("SIP<-Catalog Response: %s", resp.String())
|
||||
return int(resp.StatusCode())
|
||||
} else if err != nil {
|
||||
GB28181Plugin.Error("SIP<-Catalog error:", zap.Error(err))
|
||||
}
|
||||
return http.StatusRequestTimeout
|
||||
}
|
||||
|
||||
func (d *Device) QueryDeviceInfo() {
|
||||
for i := time.Duration(5); i < 100; i++ {
|
||||
|
||||
time.Sleep(time.Second * i)
|
||||
request := d.CreateRequest(sip.MESSAGE)
|
||||
contentType := sip.ContentType("Application/MANSCDP+xml")
|
||||
request.AppendHeader(&contentType)
|
||||
request.SetBody(BuildDeviceInfoXML(d.sn, d.ID), true)
|
||||
|
||||
response, _ := d.SipRequestForResponse(request)
|
||||
if response != nil {
|
||||
// via, _ := response.ViaHop()
|
||||
|
||||
// if via != nil && via.Params.Has("received") {
|
||||
// received, _ := via.Params.Get("received")
|
||||
// d.SipIP = received.String()
|
||||
// }
|
||||
d.Info("QueryDeviceInfo", zap.Uint16("status code", uint16(response.StatusCode())))
|
||||
if response.StatusCode() == http.StatusOK {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) SipRequestForResponse(request sip.Request) (sip.Response, error) {
|
||||
return srv.RequestWithContext(context.Background(), request)
|
||||
}
|
||||
|
||||
// MobilePositionSubscribe 移动位置订阅
|
||||
func (d *Device) MobilePositionSubscribe(id string, expires time.Duration, interval time.Duration) (code int) {
|
||||
mobilePosition := d.CreateRequest(sip.SUBSCRIBE)
|
||||
if d.subscriber.CallID != "" {
|
||||
callId := sip.CallID(utils.RandNumString(10))
|
||||
mobilePosition.ReplaceHeaders(callId.Name(), []sip.Header{&callId})
|
||||
}
|
||||
expiresHeader := sip.Expires(expires / time.Second)
|
||||
d.subscriber.Timeout = time.Now().Add(expires)
|
||||
contentType := sip.ContentType("Application/MANSCDP+xml")
|
||||
mobilePosition.AppendHeader(&contentType)
|
||||
mobilePosition.AppendHeader(&expiresHeader)
|
||||
|
||||
mobilePosition.SetBody(BuildDevicePositionXML(d.sn, id, int(interval/time.Second)), true)
|
||||
|
||||
response, err := d.SipRequestForResponse(mobilePosition)
|
||||
if err == nil && response != nil {
|
||||
if response.StatusCode() == http.StatusOK {
|
||||
callId, _ := mobilePosition.CallID()
|
||||
d.subscriber.CallID = callId.String()
|
||||
} else {
|
||||
d.subscriber.CallID = ""
|
||||
}
|
||||
return int(response.StatusCode())
|
||||
}
|
||||
return http.StatusRequestTimeout
|
||||
}
|
||||
|
||||
// UpdateChannelPosition 更新通道GPS坐标
|
||||
func (d *Device) UpdateChannelPosition(channelId string, gpsTime string, lng string, lat string) {
|
||||
if v, ok := d.channelMap.Load(channelId); ok {
|
||||
c := v.(*Channel)
|
||||
c.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
|
||||
c.Longitude = lng
|
||||
c.Latitude = lat
|
||||
c.Debug("update channel position success")
|
||||
} else {
|
||||
//如果未找到通道,则更新到设备上
|
||||
d.GpsTime = time.Now() //时间取系统收到的时间,避免设备时间和格式问题
|
||||
d.Longitude = lng
|
||||
d.Latitude = lat
|
||||
d.Debug("update device position success", zap.String("channelId", channelId))
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateChannelStatus 目录订阅消息处理:新增/移除/更新通道或者更改通道状态
|
||||
func (d *Device) UpdateChannelStatus(deviceList []*notifyMessage) {
|
||||
for _, v := range deviceList {
|
||||
switch v.Event {
|
||||
case "ON":
|
||||
d.Debug("receive channel online notify")
|
||||
d.channelOnline(v.DeviceID)
|
||||
case "OFF":
|
||||
d.Debug("receive channel offline notify")
|
||||
d.channelOffline(v.DeviceID)
|
||||
case "VLOST":
|
||||
d.Debug("receive channel video lost notify")
|
||||
d.channelOffline(v.DeviceID)
|
||||
case "DEFECT":
|
||||
d.Debug("receive channel video defect notify")
|
||||
d.channelOffline(v.DeviceID)
|
||||
case "ADD":
|
||||
d.Debug("receive channel add notify")
|
||||
channel := ChannelInfo{
|
||||
DeviceID: v.DeviceID,
|
||||
ParentID: v.ParentID,
|
||||
Name: v.Name,
|
||||
Manufacturer: v.Manufacturer,
|
||||
Model: v.Model,
|
||||
Owner: v.Owner,
|
||||
CivilCode: v.CivilCode,
|
||||
Address: v.Address,
|
||||
Port: v.Port,
|
||||
Parental: v.Parental,
|
||||
SafetyWay: v.SafetyWay,
|
||||
RegisterWay: v.RegisterWay,
|
||||
Secrecy: v.Secrecy,
|
||||
Status: ChannelStatus(v.Status),
|
||||
}
|
||||
d.addOrUpdateChannel(channel)
|
||||
case "DEL":
|
||||
//删除
|
||||
d.Debug("receive channel delete notify")
|
||||
d.deleteChannel(v.DeviceID)
|
||||
case "UPDATE":
|
||||
d.Debug("receive channel update notify")
|
||||
// 更新通道
|
||||
channel := ChannelInfo{
|
||||
DeviceID: v.DeviceID,
|
||||
ParentID: v.ParentID,
|
||||
Name: v.Name,
|
||||
Manufacturer: v.Manufacturer,
|
||||
Model: v.Model,
|
||||
Owner: v.Owner,
|
||||
CivilCode: v.CivilCode,
|
||||
Address: v.Address,
|
||||
Port: v.Port,
|
||||
Parental: v.Parental,
|
||||
SafetyWay: v.SafetyWay,
|
||||
RegisterWay: v.RegisterWay,
|
||||
Secrecy: v.Secrecy,
|
||||
Status: ChannelStatus(v.Status),
|
||||
}
|
||||
d.UpdateChannels(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) channelOnline(DeviceID string) {
|
||||
if v, ok := d.channelMap.Load(DeviceID); ok {
|
||||
c := v.(*Channel)
|
||||
c.Status = ChannelOnStatus
|
||||
c.Debug("channel online", zap.String("channelId", DeviceID))
|
||||
} else {
|
||||
d.Debug("update channel status failed, not found", zap.String("channelId", DeviceID))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Device) channelOffline(DeviceID string) {
|
||||
if v, ok := d.channelMap.Load(DeviceID); ok {
|
||||
c := v.(*Channel)
|
||||
c.Status = ChannelOffStatus
|
||||
c.Debug("channel offline", zap.String("channelId", DeviceID))
|
||||
} else {
|
||||
d.Debug("update channel status failed, not found", zap.String("channelId", DeviceID))
|
||||
}
|
||||
}
|
||||
64
go.mod
Normal file
64
go.mod
Normal file
@@ -0,0 +1,64 @@
|
||||
module m7s.live/plugin/gb28181/v4
|
||||
|
||||
go 1.19
|
||||
|
||||
require (
|
||||
github.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44
|
||||
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible
|
||||
github.com/pion/rtp v1.8.0
|
||||
go.uber.org/zap v1.24.0
|
||||
golang.org/x/net v0.12.0
|
||||
golang.org/x/text v0.11.0
|
||||
m7s.live/engine/v4 v4.13.8
|
||||
m7s.live/plugin/ps/v4 v4.0.9
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bluenviron/mediacommon v0.7.0 // indirect
|
||||
github.com/cnotch/ipchub v1.1.0 // indirect
|
||||
github.com/denisbrodbeck/machineid v1.0.1 // indirect
|
||||
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca // indirect
|
||||
github.com/fsnotify/fsnotify v1.6.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
|
||||
github.com/gobwas/httphead v0.1.0 // indirect
|
||||
github.com/gobwas/pool v0.2.1 // indirect
|
||||
github.com/gobwas/ws v1.2.1 // indirect
|
||||
github.com/golang/mock v1.6.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mcuadros/go-defaults v1.2.0 // indirect
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.2.0 // indirect
|
||||
github.com/pion/randutil v0.1.0 // indirect
|
||||
github.com/pion/webrtc/v3 v3.1.56 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
|
||||
github.com/q191201771/naza v0.30.8 // indirect
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0 // indirect
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0 // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0 // indirect
|
||||
github.com/quic-go/quic-go v0.32.0 // indirect
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.22.11 // indirect
|
||||
github.com/sirupsen/logrus v1.9.0 // indirect
|
||||
github.com/tevino/abool v1.2.0 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.11 // indirect
|
||||
github.com/tklauser/numcpus v0.6.0 // indirect
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 // indirect
|
||||
github.com/yapingcat/gomedia v0.0.0-20230727105416-c491e66c9d2a // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.2 // indirect
|
||||
go.uber.org/atomic v1.10.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
golang.org/x/crypto v0.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
|
||||
golang.org/x/mod v0.8.0 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.10.0 // indirect
|
||||
golang.org/x/term v0.10.0 // indirect
|
||||
golang.org/x/tools v0.6.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
334
go.sum
Normal file
334
go.sum
Normal file
@@ -0,0 +1,334 @@
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
|
||||
github.com/bluenviron/mediacommon v0.7.0 h1:dJWLLL9oDbAqfK8KuNfnDUQwNbeMAtGeRjZc9Vo95js=
|
||||
github.com/bluenviron/mediacommon v0.7.0/go.mod h1:wuLJdxcITiSPgY1MvQqrX+qPlKmNfeV9wNvXth5M98I=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cnotch/apirouter v0.0.0-20200731232942-89e243a791f3/go.mod h1:5deJPLON/x/s2dLOQfuKS0lenhOIT4xX0pvtN/OEIuY=
|
||||
github.com/cnotch/ipchub v1.1.0 h1:hH0lh2mU3AZXPiqMwA0pdtqrwo7PFIMRGush9OobMUs=
|
||||
github.com/cnotch/ipchub v1.1.0/go.mod h1:2PbeBs2q2VxxTVCn1eYCDwpAWuVXbq1+N0FU7GimOH4=
|
||||
github.com/cnotch/loader v0.0.0-20200405015128-d9d964d09439/go.mod h1:oWpDagHB6p+Kqqq7RoRZKyC4XAXft50hR8pbTxdbYYs=
|
||||
github.com/cnotch/queue v0.0.0-20200326024423-6e88bdbf2ad4/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg=
|
||||
github.com/cnotch/queue v0.0.0-20201224060551-4191569ce8f6/go.mod h1:zOssjAlNusOxvtaqT+EMA+Iyi8rrtKr4/XfzN1Fgoeg=
|
||||
github.com/cnotch/scheduler v0.0.0-20200522024700-1d2da93eefc5/go.mod h1:F4GE3SZkJZ8an1Y0ZCqvSM3jeozNuKzoC67erG1PhIo=
|
||||
github.com/cnotch/xlog v0.0.0-20201208005456-cfda439cd3a0/go.mod h1:RW9oHsR79ffl3sR3yMGgxYupMn2btzdtJUwoxFPUE5E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
|
||||
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
|
||||
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca h1:cTTdXpkQ1aVbOOmHwdwtYuwUZcQtcMrleD1UXLWhAq8=
|
||||
github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0=
|
||||
github.com/emitter-io/address v1.0.0/go.mod h1:GfZb5+S/o8694B1GMGK2imUYQyn2skszMvGNA5D84Ug=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
|
||||
github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
|
||||
github.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44 h1:m4/46V6uAJ95CLimMRHJjiH5psW1JuL+iLeUBzF2r70=
|
||||
github.com/ghettovoice/gosip v0.0.0-20230802091127-d58873a3fe44/go.mod h1:rlD1yLOErWYohWTryG/2bTTpmzB79p52ntLA/uIFXeI=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
|
||||
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||
github.com/gobwas/ws v1.1.0-rc.1/go.mod h1:nzvNcVha5eUziGrbxFCo6qFIojQHjJV5cLYIbezhfL0=
|
||||
github.com/gobwas/ws v1.2.1 h1:F2aeBZrm2NDsc7vbovKrWSogd4wvfAxg0FQ89/iqOTk=
|
||||
github.com/gobwas/ws v1.2.1/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 h1:yAJXTCF9TqKcTiHJAE8dj7HMvPfh66eeA2JYW7eFpSE=
|
||||
github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
|
||||
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8 h1:4Jk58quTZmzJcTrLlbB5L1Q6qXu49EIjCReWxcBFWKo=
|
||||
github.com/husanpao/ip v0.0.0-20220711082147-73160bb611a8/go.mod h1:medl9/CfYoQlqAXtAARmMW5dAX2UOdwwkhaszYPk0AM=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/kelindar/process v0.0.0-20170730150328-69a29e249ec3/go.mod h1:+lTCLnZFXOkqwD8sLPl6u4erAc0cP8wFegQHfipz7KE=
|
||||
github.com/kelindar/rate v1.0.0/go.mod h1:AjT4G+hTItNwt30lucEGZIz8y7Uk5zPho6vurIZ+1Es=
|
||||
github.com/kelindar/tcp v1.0.0/go.mod h1:JB5hj1cshLU60XrLij2BBxW3JQ4hOye8vqbyvuKb52k=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
|
||||
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mcuadros/go-defaults v1.2.0 h1:FODb8WSf0uGaY8elWJAkoLL0Ri6AlZ1bFlenk56oZtc=
|
||||
github.com/mcuadros/go-defaults v1.2.0/go.mod h1:WEZtHEVIGYVDqkKSWBdWKUVdRyKlMfulPaGDWIVeCWY=
|
||||
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
|
||||
github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.5/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
|
||||
github.com/onsi/ginkgo v1.14.2/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY=
|
||||
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI=
|
||||
github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
|
||||
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
|
||||
github.com/onsi/gomega v1.10.4/go.mod h1:g/HbgYopi++010VEqkFgJHKC09uJiW9UkXvMUuKHUCQ=
|
||||
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
|
||||
github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q=
|
||||
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
|
||||
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
|
||||
github.com/pion/ice/v2 v2.3.1/go.mod h1:aq2kc6MtYNcn4XmMhobAv6hTNJiHzvD0yXRz80+bnP8=
|
||||
github.com/pion/interceptor v0.1.12/go.mod h1:bDtgAD9dRkBZpWHGKaoKb42FhDHTG2rX8Ii9LRALLVA=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/mdns v0.0.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8=
|
||||
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||
github.com/pion/rtcp v1.2.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I=
|
||||
github.com/pion/rtp v1.6.2/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
|
||||
github.com/pion/rtp v1.8.0 h1:SYD7040IR+NqrGBOc2GDU5iDjAR+0m5rnX/EWCUMNhw=
|
||||
github.com/pion/rtp v1.8.0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU=
|
||||
github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sctp v1.8.6/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0=
|
||||
github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
|
||||
github.com/pion/srtp/v2 v2.0.12/go.mod h1:C3Ep44hlOo2qEYaq4ddsmK5dL63eLehXFbHaZ9F5V9Y=
|
||||
github.com/pion/stun v0.4.0/go.mod h1:QPsh1/SbXASntw3zkkrIk3ZJVKz4saBY2G7S10P3wCw=
|
||||
github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI=
|
||||
github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc=
|
||||
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
|
||||
github.com/pion/turn/v2 v2.1.0/go.mod h1:yrT5XbXSGX1VFSF31A3c1kCNB5bBZgk/uu5LET162qs=
|
||||
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
|
||||
github.com/pion/webrtc/v3 v3.1.56 h1:ScaiqKQN3liQwT+kJwOBaYP6TwSfixzdUnZmzHAo0a0=
|
||||
github.com/pion/webrtc/v3 v3.1.56/go.mod h1:7VhbA6ihqJlz6R/INHjyh1b8HpiV9Ct4UQvE1OB/xoM=
|
||||
github.com/pixelbender/go-sdp v1.1.0/go.mod h1:6IBlz9+BrUHoFTea7gcp4S54khtOhjCW/nVDLhmZBAs=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
|
||||
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/q191201771/naza v0.30.8 h1:Lhh29o65C4PmTDj2l+eKfsw9dddpgWZk4bFICtcnSaA=
|
||||
github.com/q191201771/naza v0.30.8/go.mod h1:n+dpJjQSh90PxBwxBNuifOwQttywvSIN5TkWSSYCeBk=
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U=
|
||||
github.com/quic-go/qtls-go1-18 v0.2.0/go.mod h1:moGulGHK7o6O8lSPSZNoOwcLvJKJ85vVNc7oJFD65bc=
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk=
|
||||
github.com/quic-go/qtls-go1-19 v0.2.0/go.mod h1:ySOI96ew8lnoKPtSqx2BlI5wCpUVPT05RMAlajtnyOI=
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI=
|
||||
github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM=
|
||||
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA=
|
||||
github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo=
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b h1:gQZ0qzfKHQIybLANtM3mBXNUtOfsCFXeTsnBqCsx1KM=
|
||||
github.com/satori/go.uuid v1.2.1-0.20181028125025-b2ce2384e17b/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0=
|
||||
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
|
||||
github.com/shirou/gopsutil/v3 v3.22.11 h1:kxsPKS+Eeo+VnEQ2XCaGJepeP6KY53QoRTETx3+1ndM=
|
||||
github.com/shirou/gopsutil/v3 v3.22.11/go.mod h1:xl0EeL4vXJ+hQMAGN8B9VFpxukEMA0XdevQOe5MZ1oY=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0=
|
||||
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
github.com/sqs/goreturns v0.0.0-20181028201513-538ac6014518/go.mod h1:CKI4AZ4XmGV240rTHfO0hfE83S6/a3/Q1siZJ/vXf7A=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||
github.com/tevino/abool v0.0.0-20170917061928-9b9efcf221b5/go.mod h1:f1SCnEOt6sc3fOJfPQDRDzHOtSXuTtnz0ImG9kPRDV0=
|
||||
github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
|
||||
github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
|
||||
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
|
||||
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
|
||||
github.com/tklauser/numcpus v0.6.0 h1:kebhY2Qt+3U6RNK7UqpYNA+tJ23IBEGKkB7JQBfDYms=
|
||||
github.com/tklauser/numcpus v0.6.0/go.mod h1:FEZLMke0lhOUG6w2JadTzp0a+Nl8PF/GFkQ5UVIcaL4=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg=
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE=
|
||||
github.com/yapingcat/gomedia v0.0.0-20230727105416-c491e66c9d2a h1:x60q0A7QmoUTzixNz7zVTdEA9JC0oYqm8S51PdbTWgs=
|
||||
github.com/yapingcat/gomedia v0.0.0-20230727105416-c491e66c9d2a/go.mod h1:WSZ59bidJOO40JSJmLqlkBJrjZCtjbKKkygEMfzY/kc=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
|
||||
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
|
||||
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
|
||||
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI=
|
||||
go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8=
|
||||
go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak=
|
||||
go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60=
|
||||
go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
|
||||
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
|
||||
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
|
||||
golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
|
||||
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201207223542-d4d67f95c62d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
|
||||
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
|
||||
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
m7s.live/engine/v4 v4.13.8 h1:pDl8YWxip5aTidw2Q4NuU+8A6irBraLRfoeBi42S6iQ=
|
||||
m7s.live/engine/v4 v4.13.8/go.mod h1:k/6iFSuJxmhJL8VO45NAga8BbgZHLLfRXOwCcCzk2s8=
|
||||
m7s.live/plugin/ps/v4 v4.0.9 h1:79iHz546EdjhH2Op5IMXsjO0SK9tUk9AFSWKo0VON3w=
|
||||
m7s.live/plugin/ps/v4 v4.0.9/go.mod h1:v59bPt1T+IxuRLRchQ+PwKkLxTRuEY4tbo13lNX6JPc=
|
||||
348
handle.go
Normal file
348
handle.go
Normal file
@@ -0,0 +1,348 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/md5"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"m7s.live/plugin/gb28181/v4/utils"
|
||||
|
||||
"github.com/ghettovoice/gosip/sip"
|
||||
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/html/charset"
|
||||
)
|
||||
|
||||
type Authorization struct {
|
||||
*sip.Authorization
|
||||
}
|
||||
|
||||
func (a *Authorization) Verify(username, passwd, realm, nonce string) bool {
|
||||
|
||||
//1、将 username,realm,password 依次组合获取 1 个字符串,并用算法加密的到密文 r1
|
||||
s1 := fmt.Sprintf("%s:%s:%s", username, realm, passwd)
|
||||
r1 := a.getDigest(s1)
|
||||
//2、将 method,即REGISTER ,uri 依次组合获取 1 个字符串,并对这个字符串使用算法 加密得到密文 r2
|
||||
s2 := fmt.Sprintf("REGISTER:%s", a.Uri())
|
||||
r2 := a.getDigest(s2)
|
||||
|
||||
if r1 == "" || r2 == "" {
|
||||
GB28181Plugin.Error("Authorization algorithm wrong")
|
||||
return false
|
||||
}
|
||||
//3、将密文 1,nonce 和密文 2 依次组合获取 1 个字符串,并对这个字符串使用算法加密,获得密文 r3,即Response
|
||||
s3 := fmt.Sprintf("%s:%s:%s", r1, nonce, r2)
|
||||
r3 := a.getDigest(s3)
|
||||
|
||||
//4、计算服务端和客户端上报的是否相等
|
||||
return r3 == a.Response()
|
||||
}
|
||||
|
||||
func (a *Authorization) getDigest(raw string) string {
|
||||
switch a.Algorithm() {
|
||||
case "MD5":
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
|
||||
default: //如果没有算法,默认使用MD5
|
||||
return fmt.Sprintf("%x", md5.Sum([]byte(raw)))
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) OnRegister(req sip.Request, tx sip.ServerTransaction) {
|
||||
from, ok := req.From()
|
||||
if !ok || from.Address == nil {
|
||||
GB28181Plugin.Error("OnRegister", zap.String("error", "no from"))
|
||||
return
|
||||
}
|
||||
id := from.Address.User().String()
|
||||
|
||||
GB28181Plugin.Debug("SIP<-OnMessage", zap.String("id", id), zap.String("source", req.Source()), zap.String("req", req.String()))
|
||||
|
||||
isUnregister := false
|
||||
if exps := req.GetHeaders("Expires"); len(exps) > 0 {
|
||||
exp := exps[0]
|
||||
expSec, err := strconv.ParseInt(exp.Value(), 10, 32)
|
||||
if err != nil {
|
||||
GB28181Plugin.Info("OnRegister",
|
||||
zap.String("error", fmt.Sprintf("wrong expire header value %q", exp)),
|
||||
zap.String("id", id),
|
||||
zap.String("source", req.Source()),
|
||||
zap.String("destination", req.Destination()))
|
||||
return
|
||||
}
|
||||
if expSec == 0 {
|
||||
isUnregister = true
|
||||
}
|
||||
} else {
|
||||
GB28181Plugin.Info("OnRegister",
|
||||
zap.String("error", "has no expire header"),
|
||||
zap.String("id", id),
|
||||
zap.String("source", req.Source()),
|
||||
zap.String("destination", req.Destination()))
|
||||
return
|
||||
}
|
||||
|
||||
GB28181Plugin.Info("OnRegister",
|
||||
zap.Bool("isUnregister", isUnregister),
|
||||
zap.String("id", id),
|
||||
zap.String("source", req.Source()),
|
||||
zap.String("destination", req.Destination()))
|
||||
|
||||
if len(id) != 20 {
|
||||
GB28181Plugin.Info("Wrong GB-28181", zap.String("id", id))
|
||||
return
|
||||
}
|
||||
passAuth := false
|
||||
// 不需要密码情况
|
||||
if c.Username == "" && c.Password == "" {
|
||||
passAuth = true
|
||||
} else {
|
||||
// 需要密码情况 设备第一次上报,返回401和加密算法
|
||||
if hdrs := req.GetHeaders("Authorization"); len(hdrs) > 0 {
|
||||
authenticateHeader := hdrs[0].(*sip.GenericHeader)
|
||||
auth := &Authorization{sip.AuthFromValue(authenticateHeader.Contents)}
|
||||
|
||||
// 有些摄像头没有配置用户名的地方,用户名就是摄像头自己的国标id
|
||||
var username string
|
||||
if auth.Username() == id {
|
||||
username = id
|
||||
} else {
|
||||
username = c.Username
|
||||
}
|
||||
|
||||
if dc, ok := DeviceRegisterCount.LoadOrStore(id, 1); ok && dc.(int) > MaxRegisterCount {
|
||||
response := sip.NewResponseFromRequest("", req, http.StatusForbidden, "Forbidden", "")
|
||||
tx.Respond(response)
|
||||
return
|
||||
} else {
|
||||
// 设备第二次上报,校验
|
||||
_nonce, loaded := DeviceNonce.Load(id)
|
||||
if loaded && auth.Verify(username, c.Password, c.Realm, _nonce.(string)) {
|
||||
passAuth = true
|
||||
} else {
|
||||
DeviceRegisterCount.Store(id, dc.(int)+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if passAuth {
|
||||
var d *Device
|
||||
if isUnregister {
|
||||
tmpd, ok := Devices.LoadAndDelete(id)
|
||||
if ok {
|
||||
GB28181Plugin.Info("Unregister Device", zap.String("id", id))
|
||||
d = tmpd.(*Device)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if v, ok := Devices.Load(id); ok {
|
||||
d = v.(*Device)
|
||||
c.RecoverDevice(d, req)
|
||||
} else {
|
||||
d = c.StoreDevice(id, req)
|
||||
}
|
||||
}
|
||||
DeviceNonce.Delete(id)
|
||||
DeviceRegisterCount.Delete(id)
|
||||
resp := sip.NewResponseFromRequest("", req, http.StatusOK, "OK", "")
|
||||
to, _ := resp.To()
|
||||
resp.ReplaceHeaders("To", []sip.Header{&sip.ToHeader{Address: to.Address, Params: sip.NewParams().Add("tag", sip.String{Str: utils.RandNumString(9)})}})
|
||||
resp.RemoveHeader("Allow")
|
||||
expires := sip.Expires(3600)
|
||||
resp.AppendHeader(&expires)
|
||||
resp.AppendHeader(&sip.GenericHeader{
|
||||
HeaderName: "Date",
|
||||
Contents: time.Now().Format(TIME_LAYOUT),
|
||||
})
|
||||
_ = tx.Respond(resp)
|
||||
|
||||
if !isUnregister {
|
||||
//订阅设备更新
|
||||
go d.syncChannels()
|
||||
}
|
||||
} else {
|
||||
GB28181Plugin.Info("OnRegister unauthorized", zap.String("id", id), zap.String("source", req.Source()),
|
||||
zap.String("destination", req.Destination()))
|
||||
response := sip.NewResponseFromRequest("", req, http.StatusUnauthorized, "Unauthorized", "")
|
||||
_nonce, _ := DeviceNonce.LoadOrStore(id, utils.RandNumString(32))
|
||||
auth := fmt.Sprintf(
|
||||
`Digest realm="%s",algorithm=%s,nonce="%s"`,
|
||||
c.Realm,
|
||||
"MD5",
|
||||
_nonce.(string),
|
||||
)
|
||||
response.AppendHeader(&sip.GenericHeader{
|
||||
HeaderName: "WWW-Authenticate",
|
||||
Contents: auth,
|
||||
})
|
||||
_ = tx.Respond(response)
|
||||
}
|
||||
}
|
||||
|
||||
// syncChannels
|
||||
// 同步设备信息、下属通道信息,包括主动查询通道信息,订阅通道变化情况
|
||||
func (d *Device) syncChannels() {
|
||||
if time.Since(d.lastSyncTime) > 2*conf.HeartbeatInterval {
|
||||
d.lastSyncTime = time.Now()
|
||||
d.Catalog()
|
||||
d.Subscribe()
|
||||
d.QueryDeviceInfo()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) OnMessage(req sip.Request, tx sip.ServerTransaction) {
|
||||
from, _ := req.From()
|
||||
id := from.Address.User().String()
|
||||
GB28181Plugin.Debug("SIP<-OnMessage", zap.String("id", id), zap.String("source", req.Source()), zap.String("req", req.String()))
|
||||
if v, ok := Devices.Load(id); ok {
|
||||
d := v.(*Device)
|
||||
switch d.Status {
|
||||
case DeviceOfflineStatus, DeviceRecoverStatus:
|
||||
c.RecoverDevice(d, req)
|
||||
go d.syncChannels()
|
||||
case DeviceRegisterStatus:
|
||||
d.Status = DeviceOnlineStatus
|
||||
}
|
||||
d.UpdateTime = time.Now()
|
||||
temp := &struct {
|
||||
XMLName xml.Name
|
||||
CmdType string
|
||||
SN int // 请求序列号,一般用于对应 request 和 response
|
||||
DeviceID string
|
||||
DeviceName string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Channel string
|
||||
DeviceList []ChannelInfo `xml:"DeviceList>Item"`
|
||||
RecordList []*Record `xml:"RecordList>Item"`
|
||||
SumNum int // 录像结果的总数 SumNum,录像结果会按照多条消息返回,可用于判断是否全部返回
|
||||
}{}
|
||||
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
|
||||
decoder.CharsetReader = charset.NewReaderLabel
|
||||
err := decoder.Decode(temp)
|
||||
if err != nil {
|
||||
err = utils.DecodeGbk(temp, []byte(req.Body()))
|
||||
if err != nil {
|
||||
GB28181Plugin.Error("decode catelog err", zap.Error(err))
|
||||
}
|
||||
}
|
||||
var body string
|
||||
switch temp.CmdType {
|
||||
case "Keepalive":
|
||||
d.LastKeepaliveAt = time.Now()
|
||||
//callID !="" 说明是订阅的事件类型信息
|
||||
if d.lastSyncTime.IsZero() {
|
||||
go d.syncChannels()
|
||||
} else {
|
||||
d.channelMap.Range(func(key, value interface{}) bool {
|
||||
if conf.InviteMode == INVIDE_MODE_AUTO {
|
||||
value.(*Channel).TryAutoInvite(&InviteOptions{})
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
//在KeepLive 进行位置订阅的处理,如果开启了自动订阅位置,则去订阅位置
|
||||
if c.Position.AutosubPosition && time.Since(d.GpsTime) > c.Position.Interval*2 {
|
||||
d.MobilePositionSubscribe(d.ID, c.Position.Expires, c.Position.Interval)
|
||||
GB28181Plugin.Debug("Mobile Position Subscribe", zap.String("deviceID", d.ID))
|
||||
}
|
||||
case "Catalog":
|
||||
d.UpdateChannels(temp.DeviceList...)
|
||||
case "RecordInfo":
|
||||
RecordQueryLink.Put(d.ID, temp.DeviceID, temp.SN, temp.SumNum, temp.RecordList)
|
||||
case "DeviceInfo":
|
||||
// 主设备信息
|
||||
d.Name = temp.DeviceName
|
||||
d.Manufacturer = temp.Manufacturer
|
||||
d.Model = temp.Model
|
||||
case "Alarm":
|
||||
d.Status = DeviceAlarmedStatus
|
||||
body = BuildAlarmResponseXML(d.ID)
|
||||
default:
|
||||
d.Warn("Not supported CmdType", zap.String("CmdType", temp.CmdType), zap.String("body", req.Body()))
|
||||
response := sip.NewResponseFromRequest("", req, http.StatusBadRequest, "", "")
|
||||
tx.Respond(response)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, "OK", body))
|
||||
} else {
|
||||
GB28181Plugin.Debug("Unauthorized message, device not found", zap.String("id", id))
|
||||
}
|
||||
}
|
||||
func (c *GB28181Config) OnBye(req sip.Request, tx sip.ServerTransaction) {
|
||||
tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, "OK", ""))
|
||||
}
|
||||
|
||||
// OnNotify 订阅通知处理
|
||||
func (c *GB28181Config) OnNotify(req sip.Request, tx sip.ServerTransaction) {
|
||||
from, _ := req.From()
|
||||
id := from.Address.User().String()
|
||||
if v, ok := Devices.Load(id); ok {
|
||||
d := v.(*Device)
|
||||
d.UpdateTime = time.Now()
|
||||
temp := &struct {
|
||||
XMLName xml.Name
|
||||
CmdType string
|
||||
DeviceID string
|
||||
Time string //位置订阅-GPS时间
|
||||
Longitude string //位置订阅-经度
|
||||
Latitude string //位置订阅-维度
|
||||
// Speed string //位置订阅-速度(km/h)(可选)
|
||||
// Direction string //位置订阅-方向(取值为当前摄像头方向与正北方的顺时针夹角,取值范围0°~360°,单位:°)(可选)
|
||||
// Altitude string //位置订阅-海拔高度,单位:m(可选)
|
||||
DeviceList []*notifyMessage `xml:"DeviceList>Item"` //目录订阅
|
||||
}{}
|
||||
decoder := xml.NewDecoder(bytes.NewReader([]byte(req.Body())))
|
||||
decoder.CharsetReader = charset.NewReaderLabel
|
||||
err := decoder.Decode(temp)
|
||||
if err != nil {
|
||||
err = utils.DecodeGbk(temp, []byte(req.Body()))
|
||||
if err != nil {
|
||||
GB28181Plugin.Error("decode catelog err", zap.Error(err))
|
||||
}
|
||||
}
|
||||
var body string
|
||||
switch temp.CmdType {
|
||||
case "Catalog":
|
||||
//目录状态
|
||||
d.UpdateChannelStatus(temp.DeviceList)
|
||||
case "MobilePosition":
|
||||
//更新channel的坐标
|
||||
d.UpdateChannelPosition(temp.DeviceID, temp.Time, temp.Longitude, temp.Latitude)
|
||||
// case "Alarm":
|
||||
// //报警事件通知 TODO
|
||||
default:
|
||||
d.Warn("Not supported CmdType", zap.String("CmdType", temp.CmdType), zap.String("body", req.Body()))
|
||||
response := sip.NewResponseFromRequest("", req, http.StatusBadRequest, "", "")
|
||||
tx.Respond(response)
|
||||
return
|
||||
}
|
||||
|
||||
tx.Respond(sip.NewResponseFromRequest("", req, http.StatusOK, "OK", body))
|
||||
}
|
||||
}
|
||||
|
||||
type notifyMessage struct {
|
||||
DeviceID string
|
||||
ParentID string
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
CivilCode string
|
||||
Address string
|
||||
Port int
|
||||
Parental int
|
||||
SafetyWay int
|
||||
RegisterWay int
|
||||
Secrecy int
|
||||
Status string
|
||||
//状态改变事件 ON:上线,OFF:离线,VLOST:视频丢失,DEFECT:故障,ADD:增加,DEL:删除,UPDATE:更新(必选)
|
||||
Event string
|
||||
}
|
||||
67
inviteoption.go
Normal file
67
inviteoption.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type InviteOptions struct {
|
||||
Start int
|
||||
End int
|
||||
dump string
|
||||
ssrc string
|
||||
SSRC uint32
|
||||
MediaPort uint16
|
||||
StreamPath string
|
||||
recyclePort func(p uint16) (err error)
|
||||
}
|
||||
|
||||
func (o InviteOptions) IsLive() bool {
|
||||
return o.Start == 0 || o.End == 0
|
||||
}
|
||||
|
||||
func (o InviteOptions) Record() bool {
|
||||
return !o.IsLive()
|
||||
}
|
||||
|
||||
func (o *InviteOptions) Validate(start, end string) error {
|
||||
if start != "" {
|
||||
sint, err1 := strconv.ParseInt(start, 10, 0)
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
o.Start = int(sint)
|
||||
}
|
||||
if end != "" {
|
||||
eint, err2 := strconv.ParseInt(end, 10, 0)
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
o.End = int(eint)
|
||||
}
|
||||
if o.Start >= o.End {
|
||||
return errors.New("start < end")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o InviteOptions) String() string {
|
||||
return fmt.Sprintf("t=%d %d", o.Start, o.End)
|
||||
}
|
||||
|
||||
func (o *InviteOptions) CreateSSRC() {
|
||||
ssrc := make([]byte, 10)
|
||||
if o.IsLive() {
|
||||
ssrc[0] = '0'
|
||||
} else {
|
||||
ssrc[0] = '1'
|
||||
}
|
||||
copy(ssrc[1:6], conf.Serial[3:8])
|
||||
randNum := 1000 + rand.Intn(8999)
|
||||
copy(ssrc[6:], strconv.Itoa(randNum))
|
||||
o.ssrc = string(ssrc)
|
||||
_ssrc, _ := strconv.ParseInt(o.ssrc, 10, 0)
|
||||
o.SSRC = uint32(_ssrc)
|
||||
}
|
||||
119
link.go
Normal file
119
link.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 对于录像查询,通过 queryKey (即 deviceId + channelId + sn) 唯一区分一次请求和响应
|
||||
// 并将其关联起来,以实现异步响应的目的
|
||||
// 提供单例实例供调用
|
||||
var RecordQueryLink = NewRecordQueryLink(time.Second * 60)
|
||||
|
||||
type recordQueryLink struct {
|
||||
pendingResult map[string]recordQueryResult // queryKey 查询结果缓存
|
||||
pendingResp map[string]recordQueryResp // queryKey 待回复的查询请求
|
||||
timeout time.Duration // 查询结果的过期时间
|
||||
sync.RWMutex
|
||||
}
|
||||
|
||||
type recordQueryResult struct {
|
||||
time time.Time
|
||||
err error
|
||||
sum int
|
||||
finished bool
|
||||
list []*Record
|
||||
}
|
||||
type recordQueryResp struct {
|
||||
respChan chan<- recordQueryResult
|
||||
timeout time.Duration
|
||||
startTime time.Time
|
||||
}
|
||||
|
||||
func NewRecordQueryLink(resultTimeout time.Duration) *recordQueryLink {
|
||||
c := &recordQueryLink{
|
||||
timeout: resultTimeout,
|
||||
pendingResult: make(map[string]recordQueryResult),
|
||||
pendingResp: make(map[string]recordQueryResp),
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// 唯一区分一次录像查询
|
||||
func recordQueryKey(deviceId, channelId string, sn int) string {
|
||||
return fmt.Sprintf("%s-%s-%d", deviceId, channelId, sn)
|
||||
}
|
||||
|
||||
// 定期清理过期的查询结果和请求
|
||||
func (c *recordQueryLink) cleanTimeout() {
|
||||
for k, s := range c.pendingResp {
|
||||
if time.Since(s.startTime) > s.timeout {
|
||||
if r, ok := c.pendingResult[k]; ok {
|
||||
c.notify(k, r)
|
||||
} else {
|
||||
c.notify(k, recordQueryResult{err: fmt.Errorf("query time out")})
|
||||
}
|
||||
}
|
||||
}
|
||||
for k, r := range c.pendingResult {
|
||||
if time.Since(r.time) > c.timeout {
|
||||
delete(c.pendingResult, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *recordQueryLink) Put(deviceId, channelId string, sn int, sum int, record []*Record) {
|
||||
key, r := c.doPut(deviceId, channelId, sn, sum, record)
|
||||
if r.finished {
|
||||
c.notify(key, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *recordQueryLink) doPut(deviceId, channelId string, sn, sum int, record []*Record) (key string, r recordQueryResult) {
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
key = recordQueryKey(deviceId, channelId, sn)
|
||||
if v, ok := c.pendingResult[key]; ok {
|
||||
r = v
|
||||
} else {
|
||||
r = recordQueryResult{time: time.Now(), sum: sum, list: make([]*Record, 0)}
|
||||
}
|
||||
|
||||
r.list = append(r.list, record...)
|
||||
if len(r.list) == sum {
|
||||
r.finished = true
|
||||
}
|
||||
c.pendingResult[key] = r
|
||||
GB28181Plugin.Logger.Debug("put record",
|
||||
zap.String("key", key),
|
||||
zap.Int("sum", sum),
|
||||
zap.Int("count", len(r.list)))
|
||||
return
|
||||
}
|
||||
|
||||
func (c *recordQueryLink) WaitResult(
|
||||
deviceId, channelId string, sn int,
|
||||
timeout time.Duration) (resultCh <-chan recordQueryResult) {
|
||||
|
||||
key := recordQueryKey(deviceId, channelId, sn)
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
respCh := make(chan recordQueryResult, 1)
|
||||
resultCh = respCh
|
||||
c.pendingResp[key] = recordQueryResp{startTime: time.Now(), timeout: timeout, respChan: respCh}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *recordQueryLink) notify(key string, r recordQueryResult) {
|
||||
if s, ok := c.pendingResp[key]; ok {
|
||||
s.respChan <- r
|
||||
}
|
||||
c.Lock()
|
||||
defer c.Unlock()
|
||||
delete(c.pendingResp, key)
|
||||
delete(c.pendingResult, key)
|
||||
GB28181Plugin.Logger.Debug("record notify", zap.String("key", key))
|
||||
}
|
||||
268
main.go
268
main.go
@@ -1,165 +1,141 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
. "github.com/Monibuca/engine/v2"
|
||||
"github.com/Monibuca/engine/v2/util"
|
||||
"github.com/Monibuca/plugin-gb28181/transaction"
|
||||
rtp "github.com/Monibuca/plugin-rtp"
|
||||
. "github.com/logrusorgru/aurora"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
myip "github.com/husanpao/ip"
|
||||
"go.uber.org/zap"
|
||||
. "m7s.live/engine/v4"
|
||||
"m7s.live/engine/v4/util"
|
||||
)
|
||||
|
||||
var Devices sync.Map
|
||||
var config = struct {
|
||||
Serial string
|
||||
Realm string
|
||||
ListenAddr string
|
||||
Expires int
|
||||
AutoInvite bool
|
||||
}{"34020000002000000001", "3402000000", "127.0.0.1:5060", 3600, true}
|
||||
|
||||
func init() {
|
||||
InstallPlugin(&PluginConfig{
|
||||
Name: "GB28181",
|
||||
Config: &config,
|
||||
Type: PLUGIN_PUBLISHER,
|
||||
Run: run,
|
||||
})
|
||||
type GB28181PositionConfig struct {
|
||||
AutosubPosition bool //是否自动订阅定位
|
||||
Expires time.Duration `default:"3600s"` //订阅周期(单位:秒)
|
||||
Interval time.Duration `default:"6s"` //订阅间隔(单位:秒)
|
||||
}
|
||||
|
||||
func run() {
|
||||
ipAddr, err := net.ResolveUDPAddr("", config.ListenAddr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
type GB28181Config struct {
|
||||
InviteMode int `default:"1"` //邀请模式,0:手动拉流,1:预拉流,2:按需拉流
|
||||
InviteIDs string //按照国标gb28181协议允许邀请的设备类型:132 摄像机 NVR
|
||||
ListenAddr string `default:"0.0.0.0"`
|
||||
//sip服务器的配置
|
||||
SipNetwork string `default:"udp"` //传输协议,默认UDP,可选TCP
|
||||
SipIP string //sip 服务器公网IP
|
||||
SipPort uint16 `default:"5060"` //sip 服务器端口,默认 5060
|
||||
Serial string `default:"34020000002000000001"` //sip 服务器 id, 默认 34020000002000000001
|
||||
Realm string `default:"3402000000"` //sip 服务器域,默认 3402000000
|
||||
Username string //sip 服务器账号
|
||||
Password string //sip 服务器密码
|
||||
Port struct { // 新配置方式
|
||||
Sip string `default:"udp:5060"`
|
||||
Media string `default:"tcp:58200-59200"`
|
||||
}
|
||||
Print(Green("server gb28181 start at"), BrightBlue(config.ListenAddr))
|
||||
config := &transaction.Config{
|
||||
SipIP: ipAddr.IP.String(),
|
||||
SipPort: uint16(ipAddr.Port),
|
||||
SipNetwork: "UDP",
|
||||
Serial: config.Serial,
|
||||
Realm: config.Realm,
|
||||
AckTimeout: 10,
|
||||
MediaIP: ipAddr.IP.String(),
|
||||
RegisterValidity: config.Expires,
|
||||
RegisterInterval: 60,
|
||||
HeartbeatInterval: 60,
|
||||
HeartbeatRetry: 3,
|
||||
// AckTimeout uint16 //sip 服务应答超时,单位秒
|
||||
RegisterValidity time.Duration `default:"3600s"` //注册有效期,单位秒,默认 3600
|
||||
// RegisterInterval int //注册间隔,单位秒,默认 60
|
||||
HeartbeatInterval time.Duration `default:"60s"` //心跳间隔,单位秒,默认 60
|
||||
// HeartbeatRetry int //心跳超时次数,默认 3
|
||||
|
||||
//媒体服务器配置
|
||||
MediaIP string //媒体服务器地址
|
||||
MediaPort uint16 `default:"58200"` //媒体服务器端口
|
||||
MediaNetwork string `default:"tcp"` //媒体传输协议,默认UDP,可选TCP
|
||||
MediaPortMin uint16 `default:"58200"`
|
||||
MediaPortMax uint16 `default:"59200"`
|
||||
// MediaIdleTimeout uint16 //推流超时时间,超过则断开链接,让设备重连
|
||||
|
||||
// WaitKeyFrame bool //是否等待关键帧,如果等待,则在收到第一个关键帧之前,忽略所有媒体流
|
||||
RemoveBanInterval time.Duration `default:"600s"` //移除禁止设备间隔
|
||||
// UdpCacheSize int //udp缓存大小
|
||||
LogLevel string `default:"info"` //trace, debug, info, warn, error, fatal, panic
|
||||
routes map[string]string
|
||||
DumpPath string //dump PS流本地文件路径
|
||||
Ignores map[string]struct{}
|
||||
tcpPorts PortManager
|
||||
udpPorts PortManager
|
||||
|
||||
Position GB28181PositionConfig //关于定位的配置参数
|
||||
|
||||
AudioEnable: true,
|
||||
WaitKeyFrame: true,
|
||||
MediaPortMin: 58200,
|
||||
MediaPortMax: 58300,
|
||||
MediaIdleTimeout: 30,
|
||||
}
|
||||
s := transaction.NewCore(config)
|
||||
s.OnInvite = onPublish
|
||||
http.HandleFunc("/gb28181/list", func(w http.ResponseWriter, r *http.Request) {
|
||||
sse := util.NewSSE(w, r.Context())
|
||||
for {
|
||||
var list []*transaction.Device
|
||||
s.Devices.Range(func(key, value interface{}) bool {
|
||||
list = append(list, value.(*transaction.Device))
|
||||
return true
|
||||
})
|
||||
sse.WriteJSON(list)
|
||||
select {
|
||||
case <-time.After(time.Second * 5):
|
||||
case <-sse.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/gb28181/control", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
id := r.URL.Query().Get("id")
|
||||
channel ,err:= strconv.Atoi(r.URL.Query().Get("channel"))
|
||||
if err!=nil{
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
ptzcmd := r.URL.Query().Get("ptzcmd")
|
||||
if v, ok := s.Devices.Load(id); ok {
|
||||
w.WriteHeader(v.(*transaction.Device).Control(channel,ptzcmd))
|
||||
} else {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/gb28181/invite", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
id := r.URL.Query().Get("id")
|
||||
channel ,err:= strconv.Atoi(r.URL.Query().Get("channel"))
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
if v, ok := s.Devices.Load(id); ok {
|
||||
w.WriteHeader(v.(*transaction.Device).Invite(channel))
|
||||
} else {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
})
|
||||
http.HandleFunc("/gb28181/bye", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
id := r.URL.Query().Get("id")
|
||||
channel ,err:= strconv.Atoi(r.URL.Query().Get("channel"))
|
||||
if err != nil {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
if v, ok := s.Devices.Load(id); ok {
|
||||
w.WriteHeader(v.(*transaction.Device).Bye(channel))
|
||||
} else {
|
||||
w.WriteHeader(404)
|
||||
}
|
||||
})
|
||||
s.Start()
|
||||
}
|
||||
|
||||
func onPublish(channel *transaction.Channel) (port int) {
|
||||
rtpPublisher := new(rtp.RTP_PS)
|
||||
if !rtpPublisher.Publish("gb28181/" + channel.DeviceID) {
|
||||
return
|
||||
func (c *GB28181Config) initRoutes() {
|
||||
c.routes = make(map[string]string)
|
||||
tempIps := myip.LocalAndInternalIPs()
|
||||
for k, v := range tempIps {
|
||||
c.routes[k] = v
|
||||
if lastdot := strings.LastIndex(k, "."); lastdot >= 0 {
|
||||
c.routes[k[0:lastdot]] = k
|
||||
}
|
||||
}
|
||||
rtpPublisher.Type = "GB28181"
|
||||
addr, err := net.ResolveUDPAddr("udp", ":0")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
networkBuffer := 1048576
|
||||
if err := conn.SetReadBuffer(networkBuffer); err != nil {
|
||||
Printf("udp server video conn set read buffer error, %v", err)
|
||||
}
|
||||
if err := conn.SetWriteBuffer(networkBuffer); err != nil {
|
||||
Printf("udp server video conn set write buffer error, %v", err)
|
||||
}
|
||||
la := conn.LocalAddr().String()
|
||||
strPort := la[strings.LastIndex(la, ":")+1:]
|
||||
if port, err = strconv.Atoi(strPort); err != nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
bufUDP := make([]byte, 1048576)
|
||||
Printf("udp server start listen video port[%d]", port)
|
||||
defer Printf("udp server stop listen video port[%d]", port)
|
||||
for rtpPublisher.Err() == nil {
|
||||
if err = conn.SetReadDeadline(time.Now().Add(time.Second*30));err!=nil{
|
||||
return
|
||||
}
|
||||
if n, _, err := conn.ReadFromUDP(bufUDP); err == nil {
|
||||
rtpPublisher.PushPS(bufUDP[:n])
|
||||
GB28181Plugin.Info("LocalAndInternalIPs", zap.Any("routes", c.routes))
|
||||
}
|
||||
|
||||
func (c *GB28181Config) OnEvent(event any) {
|
||||
switch e := event.(type) {
|
||||
case FirstConfig:
|
||||
if c.Port.Sip != "udp:5060" {
|
||||
protocol, ports := util.Conf2Listener(c.Port.Sip)
|
||||
c.SipNetwork = protocol
|
||||
c.SipPort = ports[0]
|
||||
}
|
||||
if c.Port.Media != "tcp:58200-59200" {
|
||||
protocol, ports := util.Conf2Listener(c.Port.Media)
|
||||
c.MediaNetwork = protocol
|
||||
if len(ports) > 1 {
|
||||
c.MediaPortMin = ports[0]
|
||||
c.MediaPortMax = ports[1]
|
||||
} else {
|
||||
Println("udp server read video pack error", err)
|
||||
rtpPublisher.Close()
|
||||
c.MediaPortMin = 0
|
||||
c.MediaPortMax = 0
|
||||
c.MediaPort = ports[0]
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
os.MkdirAll(c.DumpPath, 0766)
|
||||
c.ReadDevices()
|
||||
go c.initRoutes()
|
||||
c.startServer()
|
||||
case *Stream:
|
||||
if c.InviteMode == INVIDE_MODE_ONSUBSCRIBE {
|
||||
//流可能是回放流,stream path是device/channel/start-end形式
|
||||
streamNames := strings.Split(e.StreamName, "/")
|
||||
if channel := FindChannel(e.AppName, streamNames[0]); channel != nil {
|
||||
opt := InviteOptions{}
|
||||
if len(streamNames) > 1 {
|
||||
last := len(streamNames) - 1
|
||||
timestr := streamNames[last]
|
||||
trange := strings.Split(timestr, "-")
|
||||
if len(trange) == 2 {
|
||||
startTime := trange[0]
|
||||
endTime := trange[1]
|
||||
opt.Validate(startTime, endTime)
|
||||
}
|
||||
}
|
||||
channel.TryAutoInvite(&opt)
|
||||
}
|
||||
}
|
||||
case SEpublish:
|
||||
if channel := FindChannel(e.Target.AppName, strings.TrimSuffix(e.Target.StreamName, "/rtsp")); channel != nil {
|
||||
channel.LiveSubSP = e.Target.Path
|
||||
}
|
||||
case SEclose:
|
||||
if channel := FindChannel(e.Target.AppName, strings.TrimSuffix(e.Target.StreamName, "/rtsp")); channel != nil {
|
||||
channel.LiveSubSP = ""
|
||||
}
|
||||
if v, ok := PullStreams.LoadAndDelete(e.Target.Path); ok {
|
||||
go v.(*PullStream).Bye()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) IsMediaNetworkTCP() bool {
|
||||
return strings.ToLower(c.MediaNetwork) == "tcp"
|
||||
}
|
||||
|
||||
var conf GB28181Config
|
||||
|
||||
var GB28181Plugin = InstallPlugin(&conf)
|
||||
var PullStreams sync.Map //拉流
|
||||
|
||||
92
manscdp.go
Normal file
92
manscdp.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
// CatalogXML 获取设备列表xml样式
|
||||
CatalogXML = `<?xml version="1.0"?><Query>
|
||||
<CmdType>Catalog</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>
|
||||
`
|
||||
// RecordInfoXML 获取录像文件列表xml样式
|
||||
RecordInfoXML = `<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>RecordInfo</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
<StartTime>%s</StartTime>
|
||||
<EndTime>%s</EndTime>
|
||||
<Secrecy>0</Secrecy>
|
||||
<Type>all</Type>
|
||||
</Query>
|
||||
`
|
||||
// DeviceInfoXML 查询设备详情xml样式
|
||||
DeviceInfoXML = `<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>DeviceInfo</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>
|
||||
`
|
||||
// DevicePositionXML 订阅设备位置
|
||||
DevicePositionXML = `<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>MobilePosition</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
<Interval>%d</Interval>
|
||||
</Query>`
|
||||
)
|
||||
|
||||
func intTotime(t int64) time.Time {
|
||||
tstr := strconv.FormatInt(t, 10)
|
||||
if len(tstr) == 10 {
|
||||
return time.Unix(t, 0)
|
||||
}
|
||||
if len(tstr) == 13 {
|
||||
return time.UnixMilli(t)
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
// BuildDeviceInfoXML 获取设备详情指令
|
||||
func BuildDeviceInfoXML(sn int, id string) string {
|
||||
return fmt.Sprintf(DeviceInfoXML, sn, id)
|
||||
}
|
||||
|
||||
// BuildCatalogXML 获取NVR下设备列表指令
|
||||
func BuildCatalogXML(sn int, id string) string {
|
||||
return fmt.Sprintf(CatalogXML, sn, id)
|
||||
}
|
||||
|
||||
// BuildRecordInfoXML 获取录像文件列表指令
|
||||
func BuildRecordInfoXML(sn int, id string, start, end int64) string {
|
||||
return fmt.Sprintf(RecordInfoXML, sn, id, intTotime(start).Format("2006-01-02T15:04:05"), intTotime(end).Format("2006-01-02T15:04:05"))
|
||||
}
|
||||
|
||||
// BuildDevicePositionXML 订阅设备位置
|
||||
func BuildDevicePositionXML(sn int, id string, interval int) string {
|
||||
return fmt.Sprintf(DevicePositionXML, sn, id, interval)
|
||||
}
|
||||
|
||||
// AlarmResponseXML alarm response xml样式
|
||||
var (
|
||||
AlarmResponseXML = `<?xml version="1.0"?>
|
||||
<Response>
|
||||
<CmdType>Alarm</CmdType>
|
||||
<SN>17430</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Response>
|
||||
`
|
||||
)
|
||||
|
||||
// BuildRecordInfoXML 获取录像文件列表指令
|
||||
func BuildAlarmResponseXML(id string) string {
|
||||
return fmt.Sprintf(AlarmResponseXML, id)
|
||||
}
|
||||
47
portmanager.go
Normal file
47
portmanager.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package gb28181
|
||||
|
||||
import "io"
|
||||
|
||||
type PortManager struct {
|
||||
recycle chan uint16
|
||||
max uint16
|
||||
pos uint16
|
||||
Valid bool
|
||||
}
|
||||
|
||||
func (pm *PortManager) Init(start, end uint16) {
|
||||
pm.pos = start - 1
|
||||
pm.max = end
|
||||
if pm.pos > 0 && pm.max > pm.pos {
|
||||
pm.Valid = true
|
||||
pm.recycle = make(chan uint16, pm.Range())
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *PortManager) Range() uint16 {
|
||||
return pm.max - pm.pos
|
||||
}
|
||||
|
||||
func (pm *PortManager) Recycle(p uint16) (err error) {
|
||||
select {
|
||||
case pm.recycle <- p:
|
||||
return nil
|
||||
default:
|
||||
return io.EOF //TODO: 换一个Error
|
||||
}
|
||||
}
|
||||
|
||||
func (pm *PortManager) GetPort() (p uint16, err error) {
|
||||
select {
|
||||
case p = <-pm.recycle:
|
||||
return
|
||||
default:
|
||||
if pm.Range() > 0 {
|
||||
pm.pos++
|
||||
p = pm.pos
|
||||
return
|
||||
} else {
|
||||
return 0, io.EOF //TODO: 换一个Error
|
||||
}
|
||||
}
|
||||
}
|
||||
47
ptz.go
Normal file
47
ptz.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package gb28181
|
||||
|
||||
import "fmt"
|
||||
|
||||
var (
|
||||
name2code = map[string]uint8{
|
||||
"stop": 0,
|
||||
"right": 1,
|
||||
"left": 2,
|
||||
"down": 4,
|
||||
"downright": 5,
|
||||
"downleft": 6,
|
||||
"up": 8,
|
||||
"upright": 9,
|
||||
"upleft": 10,
|
||||
"zoomin": 16,
|
||||
"zoomout": 32,
|
||||
}
|
||||
)
|
||||
|
||||
func toPtzStrByCmdName(cmdName string, horizontalSpeed, verticalSpeed, zoomSpeed uint8) (string, error) {
|
||||
c, err := toPtzCode(cmdName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return toPtzStr(c, horizontalSpeed, verticalSpeed, zoomSpeed), nil
|
||||
}
|
||||
|
||||
func toPtzStr(cmdCode, horizontalSpeed, verticalSpeed, zoomSpeed uint8) string {
|
||||
checkCode := uint16(0xA5+0x0F+0x01+cmdCode+horizontalSpeed+verticalSpeed+(zoomSpeed&0xF0)) % 0x100
|
||||
|
||||
return fmt.Sprintf("A50F01%02X%02X%02X%01X0%02X",
|
||||
cmdCode,
|
||||
horizontalSpeed,
|
||||
verticalSpeed,
|
||||
zoomSpeed>>4, // 根据 GB28181 协议,zoom 只取 4 bit
|
||||
checkCode,
|
||||
)
|
||||
}
|
||||
|
||||
func toPtzCode(cmd string) (uint8, error) {
|
||||
if code, ok := name2code[cmd]; ok {
|
||||
return code, nil
|
||||
} else {
|
||||
return 0, fmt.Errorf("invalid ptz cmd %q", cmd)
|
||||
}
|
||||
}
|
||||
263
restful.go
Normal file
263
restful.go
Normal file
@@ -0,0 +1,263 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"m7s.live/engine/v4/util"
|
||||
)
|
||||
|
||||
var (
|
||||
playScaleValues = map[float32]bool{0.25: true, 0.5: true, 1: true, 2: true, 4: true}
|
||||
)
|
||||
|
||||
func (c *GB28181Config) API_list(w http.ResponseWriter, r *http.Request) {
|
||||
util.ReturnJson(func() (list []*Device) {
|
||||
list = make([]*Device, 0)
|
||||
Devices.Range(func(key, value interface{}) bool {
|
||||
list = append(list, value.(*Device))
|
||||
return true
|
||||
})
|
||||
return
|
||||
}, time.Second*5, w, r)
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_records(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
id := query.Get("id")
|
||||
channel := query.Get("channel")
|
||||
startTime := query.Get("startTime")
|
||||
endTime := query.Get("endTime")
|
||||
trange := strings.Split(query.Get("range"), "-")
|
||||
if len(trange) == 2 {
|
||||
startTime = trange[0]
|
||||
endTime = trange[1]
|
||||
}
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
res, err := c.QueryRecord(startTime, endTime)
|
||||
if err == nil {
|
||||
WriteJSONOk(w, res)
|
||||
} else {
|
||||
WriteJSON(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_control(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
channel := r.URL.Query().Get("channel")
|
||||
ptzcmd := r.URL.Query().Get("ptzcmd")
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
w.WriteHeader(c.Control(ptzcmd))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_ptz(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
id := q.Get("id")
|
||||
channel := q.Get("channel")
|
||||
cmd := q.Get("cmd") // 命令名称,见 ptz.go name2code 定义
|
||||
hs := q.Get("hSpeed") // 水平速度
|
||||
vs := q.Get("vSpeed") // 垂直速度
|
||||
zs := q.Get("zSpeed") // 缩放速度
|
||||
|
||||
hsN, err := strconv.ParseUint(hs, 10, 8)
|
||||
if err != nil {
|
||||
WriteJSON(w, "hSpeed parameter is invalid", 400)
|
||||
return
|
||||
}
|
||||
vsN, err := strconv.ParseUint(vs, 10, 8)
|
||||
if err != nil {
|
||||
WriteJSON(w, "vSpeed parameter is invalid", 400)
|
||||
return
|
||||
}
|
||||
zsN, err := strconv.ParseUint(zs, 10, 8)
|
||||
if err != nil {
|
||||
WriteJSON(w, "zSpeed parameter is invalid", 400)
|
||||
return
|
||||
}
|
||||
|
||||
ptzcmd, err := toPtzStrByCmdName(cmd, uint8(hsN), uint8(vsN), uint8(zsN))
|
||||
if err != nil {
|
||||
WriteJSON(w, err.Error(), 400)
|
||||
return
|
||||
}
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
code := c.Control(ptzcmd)
|
||||
WriteJSON(w, "device received", code)
|
||||
} else {
|
||||
WriteJSON(w, fmt.Sprintf("device %q channel %q not found", id, channel), 404)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_invite(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
id := query.Get("id")
|
||||
channel := query.Get("channel")
|
||||
streamPath := query.Get("streamPath")
|
||||
port, _ := strconv.Atoi(query.Get("mediaPort"))
|
||||
opt := InviteOptions{
|
||||
dump: query.Get("dump"),
|
||||
MediaPort: uint16(port),
|
||||
StreamPath: streamPath,
|
||||
}
|
||||
startTime := query.Get("startTime")
|
||||
endTime := query.Get("endTime")
|
||||
trange := strings.Split(query.Get("range"), "-")
|
||||
if len(trange) == 2 {
|
||||
startTime = trange[0]
|
||||
endTime = trange[1]
|
||||
}
|
||||
opt.Validate(startTime, endTime)
|
||||
if c := FindChannel(id, channel); c == nil {
|
||||
http.NotFound(w, r)
|
||||
} else if opt.IsLive() && c.status.Load() > 0 {
|
||||
http.Error(w, "live stream already exists", http.StatusNotModified)
|
||||
} else if code, err := c.Invite(&opt); err == nil {
|
||||
w.WriteHeader(code)
|
||||
} else {
|
||||
http.Error(w, err.Error(), code)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_bye(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
channel := r.URL.Query().Get("channel")
|
||||
streamPath := r.URL.Query().Get("streamPath")
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
w.WriteHeader(c.Bye(streamPath))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_play_pause(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
channel := r.URL.Query().Get("channel")
|
||||
streamPath := r.URL.Query().Get("streamPath")
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
w.WriteHeader(c.Pause(streamPath))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_play_resume(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
channel := r.URL.Query().Get("channel")
|
||||
streamPath := r.URL.Query().Get("streamPath")
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
w.WriteHeader(c.Resume(streamPath))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_play_seek(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
channel := r.URL.Query().Get("channel")
|
||||
streamPath := r.URL.Query().Get("streamPath")
|
||||
secStr := r.URL.Query().Get("second")
|
||||
sec, err := strconv.ParseUint(secStr, 10, 32)
|
||||
if err != nil {
|
||||
WriteJSON(w, "second parameter is invalid: "+err.Error(), 400)
|
||||
return
|
||||
}
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
w.WriteHeader(c.PlayAt(streamPath, uint(sec)))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_play_forward(w http.ResponseWriter, r *http.Request) {
|
||||
id := r.URL.Query().Get("id")
|
||||
channel := r.URL.Query().Get("channel")
|
||||
streamPath := r.URL.Query().Get("streamPath")
|
||||
speedStr := r.URL.Query().Get("speed")
|
||||
speed, err := strconv.ParseFloat(speedStr, 32)
|
||||
secondErrMsg := "speed parameter is invalid, should be one of 0.25,0.5,1,2,4"
|
||||
if err != nil || !playScaleValues[float32(speed)] {
|
||||
WriteJSON(w, secondErrMsg, 400)
|
||||
return
|
||||
}
|
||||
if c := FindChannel(id, channel); c != nil {
|
||||
w.WriteHeader(c.PlayForward(streamPath, float32(speed)))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_position(w http.ResponseWriter, r *http.Request) {
|
||||
//CORS(w, r)
|
||||
query := r.URL.Query()
|
||||
//设备id
|
||||
id := query.Get("id")
|
||||
//订阅周期(单位:秒)
|
||||
expires := query.Get("expires")
|
||||
//订阅间隔(单位:秒)
|
||||
interval := query.Get("interval")
|
||||
|
||||
expiresInt, err := time.ParseDuration(expires)
|
||||
if expires == "" || err != nil {
|
||||
expiresInt = c.Position.Expires
|
||||
}
|
||||
intervalInt, err := time.ParseDuration(interval)
|
||||
if interval == "" || err != nil {
|
||||
intervalInt = c.Position.Interval
|
||||
}
|
||||
|
||||
if v, ok := Devices.Load(id); ok {
|
||||
d := v.(*Device)
|
||||
w.WriteHeader(d.MobilePositionSubscribe(id, expiresInt, intervalInt))
|
||||
} else {
|
||||
http.NotFound(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
type DevicePosition struct {
|
||||
ID string
|
||||
GpsTime time.Time //gps时间
|
||||
Longitude string //经度
|
||||
Latitude string //纬度
|
||||
}
|
||||
|
||||
func (c *GB28181Config) API_get_position(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
//设备id
|
||||
id := query.Get("id")
|
||||
|
||||
util.ReturnJson(func() (list []*DevicePosition) {
|
||||
if id == "" {
|
||||
Devices.Range(func(key, value interface{}) bool {
|
||||
d := value.(*Device)
|
||||
if time.Since(d.GpsTime) <= c.Position.Interval {
|
||||
list = append(list, &DevicePosition{ID: d.ID, GpsTime: d.GpsTime, Longitude: d.Longitude, Latitude: d.Latitude})
|
||||
}
|
||||
return true
|
||||
})
|
||||
} else if v, ok := Devices.Load(id); ok {
|
||||
d := v.(*Device)
|
||||
list = append(list, &DevicePosition{ID: d.ID, GpsTime: d.GpsTime, Longitude: d.Longitude, Latitude: d.Latitude})
|
||||
}
|
||||
return
|
||||
}, c.Position.Interval, w, r)
|
||||
}
|
||||
|
||||
func WriteJSONOk(w http.ResponseWriter, data interface{}) {
|
||||
WriteJSON(w, data, 200)
|
||||
}
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, data interface{}, status int) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(data)
|
||||
}
|
||||
217
server.go
Executable file
217
server.go
Executable file
@@ -0,0 +1,217 @@
|
||||
package gb28181
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/logrusorgru/aurora"
|
||||
"go.uber.org/zap"
|
||||
"m7s.live/plugin/gb28181/v4/utils"
|
||||
|
||||
"github.com/ghettovoice/gosip"
|
||||
"github.com/ghettovoice/gosip/log"
|
||||
"github.com/ghettovoice/gosip/sip"
|
||||
)
|
||||
|
||||
var srv gosip.Server
|
||||
|
||||
const MaxRegisterCount = 3
|
||||
|
||||
func FindChannel(deviceId string, channelId string) (c *Channel) {
|
||||
if v, ok := Devices.Load(deviceId); ok {
|
||||
d := v.(*Device)
|
||||
if v, ok := d.channelMap.Load(channelId); ok {
|
||||
return v.(*Channel)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var levelMap = map[string]log.Level{
|
||||
"trace": log.TraceLevel,
|
||||
"debug": log.DebugLevel,
|
||||
"info": log.InfoLevel,
|
||||
"warn": log.WarnLevel,
|
||||
"error": log.ErrorLevel,
|
||||
"fatal": log.FatalLevel,
|
||||
"panic": log.PanicLevel,
|
||||
}
|
||||
|
||||
func GetSipServer(transport string) gosip.Server {
|
||||
return srv
|
||||
}
|
||||
|
||||
var sn = 0
|
||||
|
||||
func CreateRequest(exposedId string, Method sip.RequestMethod, recipient *sip.Address, netAddr string) (req sip.Request) {
|
||||
|
||||
sn++
|
||||
|
||||
callId := sip.CallID(utils.RandNumString(10))
|
||||
userAgent := sip.UserAgentHeader("Monibuca")
|
||||
cseq := sip.CSeq{
|
||||
SeqNo: uint32(sn),
|
||||
MethodName: Method,
|
||||
}
|
||||
port := sip.Port(conf.SipPort)
|
||||
serverAddr := sip.Address{
|
||||
//DisplayName: sip.String{Str: d.config.Serial},
|
||||
Uri: &sip.SipUri{
|
||||
FUser: sip.String{Str: exposedId},
|
||||
FHost: conf.SipIP,
|
||||
FPort: &port,
|
||||
},
|
||||
Params: sip.NewParams().Add("tag", sip.String{Str: utils.RandNumString(9)}),
|
||||
}
|
||||
req = sip.NewRequest(
|
||||
"",
|
||||
Method,
|
||||
recipient.Uri,
|
||||
"SIP/2.0",
|
||||
[]sip.Header{
|
||||
serverAddr.AsFromHeader(),
|
||||
recipient.AsToHeader(),
|
||||
&callId,
|
||||
&userAgent,
|
||||
&cseq,
|
||||
serverAddr.AsContactHeader(),
|
||||
},
|
||||
"",
|
||||
nil,
|
||||
)
|
||||
|
||||
req.SetTransport(conf.SipNetwork)
|
||||
req.SetDestination(netAddr)
|
||||
//fmt.Printf("构建请求参数:%s", *&req)
|
||||
// requestMsg.DestAdd, err2 = d.ResolveAddress(requestMsg)
|
||||
// if err2 != nil {
|
||||
// return nil
|
||||
// }
|
||||
//intranet ip , let's resolve it with public ip
|
||||
// var deviceIp, deviceSourceIP net.IP
|
||||
// switch addr := requestMsg.DestAdd.(type) {
|
||||
// case *net.UDPAddr:
|
||||
// deviceIp = addr.IP
|
||||
// case *net.TCPAddr:
|
||||
// deviceIp = addr.IP
|
||||
// }
|
||||
|
||||
// switch addr2 := d.SourceAddr.(type) {
|
||||
// case *net.UDPAddr:
|
||||
// deviceSourceIP = addr2.IP
|
||||
// case *net.TCPAddr:
|
||||
// deviceSourceIP = addr2.IP
|
||||
// }
|
||||
// if deviceIp.IsPrivate() && !deviceSourceIP.IsPrivate() {
|
||||
// requestMsg.DestAdd = d.SourceAddr
|
||||
// }
|
||||
return
|
||||
}
|
||||
func RequestForResponse(transport string, request sip.Request,
|
||||
options ...gosip.RequestWithContextOption) (sip.Response, error) {
|
||||
return (GetSipServer(transport)).RequestWithContext(context.Background(), request, options...)
|
||||
}
|
||||
|
||||
func (c *GB28181Config) startServer() {
|
||||
addr := c.ListenAddr + ":" + strconv.Itoa(int(c.SipPort))
|
||||
|
||||
logger := utils.NewZapLogger(GB28181Plugin.Logger, "GB SIP Server", nil)
|
||||
logger.SetLevel(levelMap[c.LogLevel])
|
||||
// logger := log.NewDefaultLogrusLogger().WithPrefix("GB SIP Server")
|
||||
srvConf := gosip.ServerConfig{}
|
||||
if c.SipIP != "" {
|
||||
srvConf.Host = c.SipIP
|
||||
}
|
||||
srv = gosip.NewServer(srvConf, nil, nil, logger)
|
||||
srv.OnRequest(sip.REGISTER, c.OnRegister)
|
||||
srv.OnRequest(sip.MESSAGE, c.OnMessage)
|
||||
srv.OnRequest(sip.NOTIFY, c.OnNotify)
|
||||
srv.OnRequest(sip.BYE, c.OnBye)
|
||||
err := srv.Listen(strings.ToLower(c.SipNetwork), addr)
|
||||
if err != nil {
|
||||
GB28181Plugin.Logger.Error("gb28181 server listen", zap.Error(err))
|
||||
} else {
|
||||
GB28181Plugin.Info(fmt.Sprint(aurora.Green("Server gb28181 start at"), aurora.BrightBlue(addr)))
|
||||
}
|
||||
|
||||
if c.MediaNetwork == "tcp" {
|
||||
c.tcpPorts.Init(c.MediaPortMin, c.MediaPortMax)
|
||||
} else {
|
||||
c.udpPorts.Init(c.MediaPortMin, c.MediaPortMax)
|
||||
}
|
||||
go c.startJob()
|
||||
}
|
||||
|
||||
// func queryCatalog(config *transaction.Config) {
|
||||
// t := time.NewTicker(time.Duration(config.CatalogInterval) * time.Second)
|
||||
// for range t.C {
|
||||
// Devices.Range(func(key, value interface{}) bool {
|
||||
// device := value.(*Device)
|
||||
// if time.Since(device.UpdateTime) > time.Duration(config.RegisterValidity)*time.Second {
|
||||
// Devices.Delete(key)
|
||||
// } else if device.Channels != nil {
|
||||
// go device.Catalog()
|
||||
// }
|
||||
// return true
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
|
||||
// 定时任务
|
||||
func (c *GB28181Config) startJob() {
|
||||
statusTick := time.NewTicker(c.HeartbeatInterval / 2)
|
||||
banTick := time.NewTicker(c.RemoveBanInterval)
|
||||
linkTick := time.NewTicker(time.Millisecond * 100)
|
||||
GB28181Plugin.Debug("start job")
|
||||
for {
|
||||
select {
|
||||
case <-banTick.C:
|
||||
if c.Username != "" || c.Password != "" {
|
||||
c.removeBanDevice()
|
||||
}
|
||||
case <-statusTick.C:
|
||||
c.statusCheck()
|
||||
case <-linkTick.C:
|
||||
RecordQueryLink.cleanTimeout()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GB28181Config) removeBanDevice() {
|
||||
DeviceRegisterCount.Range(func(key, value interface{}) bool {
|
||||
if value.(int) > MaxRegisterCount {
|
||||
DeviceRegisterCount.Delete(key)
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
// statusCheck
|
||||
// - 当设备超过 3 倍心跳时间未发送过心跳(通过 UpdateTime 判断), 视为离线
|
||||
// - 当设备超过注册有效期内为发送过消息,则从设备列表中删除
|
||||
// UpdateTime 在设备发送心跳之外的消息也会被更新,相对于 LastKeepaliveAt 更能体现出设备最会一次活跃的时间
|
||||
func (c *GB28181Config) statusCheck() {
|
||||
Devices.Range(func(key, value any) bool {
|
||||
d := value.(*Device)
|
||||
if time.Since(d.UpdateTime) > c.RegisterValidity {
|
||||
Devices.Delete(key)
|
||||
GB28181Plugin.Info("Device register timeout",
|
||||
zap.String("id", d.ID),
|
||||
zap.Time("registerTime", d.RegisterTime),
|
||||
zap.Time("updateTime", d.UpdateTime),
|
||||
)
|
||||
} else if time.Since(d.UpdateTime) > c.HeartbeatInterval*3 {
|
||||
d.Status = DeviceOfflineStatus
|
||||
d.channelMap.Range(func(key, value any) bool {
|
||||
ch := value.(*Channel)
|
||||
ch.Status = ChannelOffStatus
|
||||
return true
|
||||
})
|
||||
GB28181Plugin.Info("Device offline", zap.String("id", d.ID), zap.Time("updateTime", d.UpdateTime))
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
120
sip/README.md
120
sip/README.md
@@ -1,120 +0,0 @@
|
||||
|
||||
#### SIP消息
|
||||
|
||||
```
|
||||
SIP-message = Request / Response
|
||||
Request = Request-Line
|
||||
*( message-header )
|
||||
CRLF
|
||||
[ message-body ]
|
||||
```
|
||||
|
||||
#### SIP消息头域
|
||||
|
||||
```
|
||||
message-header = (Accept
|
||||
/ Accept-Encoding
|
||||
/ Accept-Language
|
||||
/ Alert-Info
|
||||
/ Allow
|
||||
/ Authentication-Info
|
||||
/ Authorization
|
||||
/ Call-ID
|
||||
/ Call-Info
|
||||
/ Contact
|
||||
/ Content-Disposition
|
||||
/ Content-Encoding
|
||||
/ Content-Language
|
||||
/ Content-Length
|
||||
/ Content-Type
|
||||
/ CSeq
|
||||
/ Date
|
||||
/ Error-Info
|
||||
/ Expires
|
||||
/ From
|
||||
/ In-Reply-To
|
||||
/ Max-Forwards
|
||||
/ MIME-Version
|
||||
/ Min-Expires
|
||||
/ Organization
|
||||
/ Priority
|
||||
/ Proxy-Authenticate
|
||||
/ Proxy-Authorization
|
||||
/ Proxy-Require
|
||||
/ Record-Route
|
||||
/ Reply-To
|
||||
/ Require
|
||||
/ Retry-After
|
||||
/ Route
|
||||
/ Server
|
||||
/ Subject
|
||||
/ Supported
|
||||
/ Timestamp
|
||||
/ To
|
||||
/ Unsupported
|
||||
/ User-Agent
|
||||
/ Via
|
||||
/ Warning
|
||||
/ WWW-Authenticate
|
||||
/ extension-header) CRLF
|
||||
```
|
||||
|
||||
|
||||
#### SIP 响应状态码
|
||||
|
||||
```
|
||||
Informational = "100" ; Trying
|
||||
/ "180" ; Ringing
|
||||
/ "181" ; Call Is Being Forwarded
|
||||
/ "182" ; Queued
|
||||
/ "183" ; Session Progress
|
||||
Success = "200" ; OK
|
||||
|
||||
Redirection = "300" ; Multiple Choices
|
||||
/ "301" ; Moved Permanently
|
||||
/ "302" ; Moved Temporarily
|
||||
/ "305" ; Use Proxy
|
||||
/ "380" ; Alternative Service
|
||||
|
||||
Client-Error = "400" ; Bad Request
|
||||
/ "401" ; Unauthorized
|
||||
/ "402" ; Payment Required
|
||||
/ "403" ; Forbidden
|
||||
/ "404" ; Not Found
|
||||
/ "405" ; Method Not Allowed
|
||||
/ "406" ; Not Acceptable
|
||||
/ "407" ; Proxy Authentication Required
|
||||
/ "408" ; Request Timeout
|
||||
/ "410" ; Gone
|
||||
/ "413" ; Request Entity Too Large
|
||||
/ "414" ; Request-URI Too Large
|
||||
/ "415" ; Unsupported Media Type
|
||||
/ "416" ; Unsupported URI Scheme
|
||||
/ "420" ; Bad Extension
|
||||
/ "421" ; Extension Required
|
||||
/ "423" ; Interval Too Brief
|
||||
/ "480" ; Temporarily not available
|
||||
/ "481" ; Call Leg/Transaction Does Not Exist
|
||||
/ "482" ; Loop Detected
|
||||
/ "483" ; Too Many Hops
|
||||
/ "484" ; Address Incomplete
|
||||
/ "485" ; Ambiguous
|
||||
/ "486" ; Busy Here
|
||||
/ "487" ; Request Terminated
|
||||
/ "488" ; Not Acceptable Here
|
||||
/ "491" ; Request Pending
|
||||
/ "493" ; Undecipherable
|
||||
|
||||
Server-Error = "500" ; Internal Server Error
|
||||
/ "501" ; Not Implemented
|
||||
/ "502" ; Bad Gateway
|
||||
/ "503" ; Service Unavailable
|
||||
/ "504" ; Server Time-out
|
||||
/ "505" ; SIP Version not supported
|
||||
/ "513" ; Message Too Large
|
||||
Global-Failure = "600" ; Busy Everywhere
|
||||
/ "603" ; Decline
|
||||
/ "604" ; Does not exist anywhere
|
||||
/ "606" ; Not Acceptable
|
||||
|
||||
```
|
||||
18
sip/body.go
18
sip/body.go
@@ -1,18 +0,0 @@
|
||||
package sip
|
||||
|
||||
//sip message body
|
||||
//xml解析的字段
|
||||
const (
|
||||
MESSAGE_CATALOG = "Catalog"
|
||||
MESSAGE_DEVICE_INFO = "DeviceInfo"
|
||||
MESSAGE_BROADCAST = "Broadcast"
|
||||
MESSAGE_DEVICE_STATUS = "DeviceStatus"
|
||||
MESSAGE_KEEP_ALIVE = "Keepalive"
|
||||
MESSAGE_MOBILE_POSITION = "MobilePosition"
|
||||
MESSAGE_MOBILE_POSITION_INTERVAL = "Interval"
|
||||
|
||||
ELEMENT_DEVICE_ID = "DeviceID"
|
||||
ELEMENT_DEVICE_LIST = "DeviceList"
|
||||
ELEMENT_NAME = "Name"
|
||||
ELEMENT_STATUS = "Status"
|
||||
)
|
||||
56
sip/demo.go
56
sip/demo.go
@@ -1,56 +0,0 @@
|
||||
package sip
|
||||
|
||||
import "fmt"
|
||||
|
||||
func DemoMessage() {
|
||||
registerStr := `REGISTER sip:34020000002000000001@3402000000 SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.1.64:5060;rport;branch=z9hG4bK385701375
|
||||
From: <sip:34020000001320000001@3402000000>;tag=1840661473
|
||||
To: <sip:34020000001320000001@3402000000>
|
||||
Call-ID: 418133739
|
||||
CSeq: 1 REGISTER
|
||||
Contact: <sip:34020000001320000001@192.168.1.64:5060>
|
||||
Max-Forwards: 70
|
||||
User-Agent: IP Camera
|
||||
Expires: 3600
|
||||
Content-Length: 0`
|
||||
|
||||
fmt.Println("input:")
|
||||
fmt.Println(registerStr)
|
||||
msg, err := Decode([]byte(registerStr))
|
||||
if err != nil {
|
||||
fmt.Println("decode message failed:", err.Error())
|
||||
return
|
||||
}
|
||||
out, err := Encode(msg)
|
||||
if err != nil {
|
||||
fmt.Println("encode message failed:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println("=====================================")
|
||||
fmt.Println("output:")
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
func DemoVIA() {
|
||||
str1 := "SIP / 2.0 / UDP first.example.com: 4000;ttl=16 ;maddr=224.2.0.1 ;branch=z9hG4bKa7c6a8dlze.1"
|
||||
str2 := "SIP/2.0/UDP 192.168.1.64:5060;rport;received=192.168.1.64;branch=z9hG4bK1000615294"
|
||||
|
||||
var err error
|
||||
v1 := &Via{}
|
||||
err = v1.Parse(str1)
|
||||
if err != nil {
|
||||
fmt.Println("error:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("result:%v\n", v1.String())
|
||||
|
||||
v2 := &Via{}
|
||||
err = v2.Parse(str2)
|
||||
if err != nil {
|
||||
fmt.Println("error:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("result:%v\n", v2.String())
|
||||
|
||||
}
|
||||
566
sip/head.go
566
sip/head.go
@@ -1,566 +0,0 @@
|
||||
package sip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//换行符号:
|
||||
//linux,unix : \r\n
|
||||
//windows : \n
|
||||
//Mac OS : \r
|
||||
const (
|
||||
VERSION = "SIP/2.0" // sip version
|
||||
CRLF = "\r\n" // 0x0D0A
|
||||
CRLFCRLF = "\r\n\r\n" // 0x0D0A0D0A
|
||||
|
||||
//CRLF = "\n" // 0x0D
|
||||
//CRLFCRLF = "\n\n" // 0x0D0D
|
||||
)
|
||||
|
||||
//SIP消息类型:请求or响应
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
SIP_MESSAGE_REQUEST Mode = 0
|
||||
SIP_MESSAGE_RESPONSE Mode = 1
|
||||
)
|
||||
|
||||
//sip request method
|
||||
type Method string
|
||||
|
||||
const (
|
||||
ACK Method = "ACK"
|
||||
BYE Method = "BYE"
|
||||
CANCEL Method = "CANCEL"
|
||||
INVITE Method = "INVITE"
|
||||
OPTIONS Method = "OPTIONS"
|
||||
REGISTER Method = "REGISTER"
|
||||
NOTIFY Method = "NOTIFY"
|
||||
SUBSCRIBE Method = "SUBSCRIBE"
|
||||
MESSAGE Method = "MESSAGE"
|
||||
REFER Method = "REFER"
|
||||
INFO Method = "INFO"
|
||||
PRACK Method = "PRACK"
|
||||
UPDATE Method = "UPDATE"
|
||||
PUBLISH Method = "PUBLISH"
|
||||
)
|
||||
|
||||
//startline
|
||||
//MESSAGE sip:34020000001320000001@3402000000 SIP/2.0
|
||||
//SIP/2.0 200 OK
|
||||
type StartLine struct {
|
||||
raw string //原始内容
|
||||
|
||||
//request line: method uri version
|
||||
Method Method
|
||||
Uri URI //Request-URI:请求的服务地址,不能包含空白字符或者控制字符,并且禁止用”<>”括上。
|
||||
Version string
|
||||
|
||||
//status line: version code phrase
|
||||
Code int //status code
|
||||
phrase string
|
||||
}
|
||||
|
||||
func (l *StartLine) String() string {
|
||||
if l.Version == "" {
|
||||
l.Version = "SIP/2.0"
|
||||
}
|
||||
var result string
|
||||
if l.Method == "" {
|
||||
result = fmt.Sprintf("%s %d %s", l.Version, l.Code, l.phrase)
|
||||
} else {
|
||||
result = fmt.Sprintf("%s %s %s", l.Method, l.Uri.String(), l.Version)
|
||||
}
|
||||
l.raw = result
|
||||
return l.raw
|
||||
}
|
||||
|
||||
//To From Referto Contact
|
||||
//From: <sip:34020000001320000001@3402000000>;tag=575945878
|
||||
//To: <sip:34020000002000000001@3402000000>
|
||||
//Contact: <sip:34020000001320000001@27.38.49.149:49243>
|
||||
//Contact: <sip:34020000001320000001@192.168.1.64:5060>;expires=0
|
||||
type Contact struct {
|
||||
raw string //原始内容
|
||||
|
||||
Nickname string //可以没有
|
||||
Uri URI //
|
||||
|
||||
//header params
|
||||
Params map[string]string // include tag/q/expires
|
||||
}
|
||||
|
||||
func (c *Contact) String() string {
|
||||
sb := strings.Builder{}
|
||||
|
||||
if c.Nickname != "" {
|
||||
sb.WriteByte('"')
|
||||
sb.WriteString(c.Nickname)
|
||||
sb.WriteByte('"')
|
||||
sb.WriteByte(' ')
|
||||
}
|
||||
urlStr := c.Uri.String()
|
||||
if strings.ContainsAny(urlStr, ",?:") {
|
||||
urlStr = fmt.Sprintf("<%s>", urlStr)
|
||||
}
|
||||
sb.WriteString(urlStr)
|
||||
|
||||
if c.Params != nil {
|
||||
for k, v := range c.Params {
|
||||
sb.WriteString(";")
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
c.raw = sb.String()
|
||||
return c.raw
|
||||
}
|
||||
|
||||
func (c *Contact) Parse(str string) (err error) {
|
||||
c.raw = str
|
||||
|
||||
if str == "*" {
|
||||
c.Uri.host = "*"
|
||||
return
|
||||
}
|
||||
|
||||
n0 := strings.IndexByte(str, '"')
|
||||
if n0 != -1 {
|
||||
str = str[n0+1:]
|
||||
n1 := strings.IndexByte(str, '"')
|
||||
if n1 == -1 {
|
||||
return errors.New("parse nickname failed")
|
||||
}
|
||||
c.Nickname = str[:n1]
|
||||
str = strings.TrimSpace(str[n1+1:])
|
||||
}
|
||||
|
||||
if len(str) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var uriDone = false
|
||||
if strings.ContainsAny(str, "<>") {
|
||||
n2 := strings.IndexByte(str, '<')
|
||||
n3 := strings.IndexByte(str, '>')
|
||||
if n2 == -1 || n3 == -1 {
|
||||
err = errors.New("parse contact-uri failed")
|
||||
return
|
||||
}
|
||||
c.Uri, err = parseURI(str[n2+1 : n3])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
uriDone = true
|
||||
str = strings.TrimSpace(str[n3+1:])
|
||||
}
|
||||
|
||||
if len(str) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
str = strings.Trim(str, ";")
|
||||
arr1 := strings.Split(str, ";")
|
||||
for idx, one := range arr1 {
|
||||
//如果上面没有通过<>解析出来uri,则用分号split的第一个元素,就是uri字符串
|
||||
if !uriDone && idx == 0 {
|
||||
c.Uri, err = parseURI(one)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
if c.Params == nil {
|
||||
c.Params = make(map[string]string)
|
||||
}
|
||||
arr2 := strings.Split(one, "=")
|
||||
k, v := arr2[0], arr2[1]
|
||||
c.Params[k] = v
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//Via: SIP/2.0/UDP 192.168.1.64:5060;rport=49243;received=27.38.49.149;branch=z9hG4bK879576192
|
||||
//Params:
|
||||
//Received : IPv4address / IPv6address
|
||||
//RPort : 0-not found, -1-no-value, other-value
|
||||
//Branch : branch参数的值必须用magic cookie "z9hG4bK" 作为开头
|
||||
|
||||
/*
|
||||
Via = ( "Via" / "v" ) HCOLON via-parm *(COMMA via-parm)
|
||||
via-parm = sent-protocol LWS sent-by *( SEMI via-params )
|
||||
via-params = via-ttl / via-maddr
|
||||
/ via-received / via-branch
|
||||
/ via-extension
|
||||
via-ttl = "ttl" EQUAL ttl
|
||||
via-maddr = "maddr" EQUAL host
|
||||
via-received = "received" EQUAL (IPv4address / IPv6address)
|
||||
via-branch = "branch" EQUAL token
|
||||
via-extension = generic-param
|
||||
sent-protocol = protocol-name SLASH protocol-version
|
||||
SLASH transport
|
||||
protocol-name = "SIP" / token
|
||||
protocol-version = token
|
||||
transport = "UDP" / "TCP" / "TLS" / "SCTP"
|
||||
/ other-transport
|
||||
sent-by = host [ COLON port ]
|
||||
ttl = 1*3DIGIT ; 0 to 255
|
||||
*/
|
||||
type Via struct {
|
||||
raw string // 原始内容
|
||||
Version string // sip version: default to SIP/2.0
|
||||
Transport string // UDP,TCP ,TLS , SCTP
|
||||
Host string // sent-by : host:port
|
||||
Port string //
|
||||
//header params
|
||||
Params map[string]string // include branch/rport/received/ttl/maddr
|
||||
}
|
||||
|
||||
func (v *Via) GetBranch() string {
|
||||
return v.Params["branch"]
|
||||
}
|
||||
|
||||
func (v *Via) GetSendBy() string {
|
||||
var host, port string
|
||||
|
||||
sb := strings.Builder{}
|
||||
received := v.Params["received"]
|
||||
rport := v.Params["rport"]
|
||||
|
||||
if received != "" {
|
||||
host = received
|
||||
} else {
|
||||
host = v.Host
|
||||
}
|
||||
|
||||
if rport != "" && rport != "0" && rport != "-1" {
|
||||
port = rport
|
||||
} else if v.Port != "" {
|
||||
port = v.Port
|
||||
} else {
|
||||
if strings.ToUpper(v.Transport) == "UDP" {
|
||||
port = "5060"
|
||||
} else {
|
||||
port = "5061"
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString(host)
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(port)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
func (v *Via) String() string {
|
||||
sb := strings.Builder{}
|
||||
if v.Version == "" {
|
||||
v.Version = "SIP/2.0"
|
||||
}
|
||||
|
||||
if v.Transport == "" {
|
||||
v.Transport = "UDP"
|
||||
}
|
||||
|
||||
sb.WriteString(v.Version)
|
||||
sb.WriteString("/")
|
||||
sb.WriteString(v.Transport)
|
||||
sb.WriteString(" ")
|
||||
sb.WriteString(v.Host)
|
||||
if v.Port != "" {
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(v.Port)
|
||||
}
|
||||
|
||||
if v.Params != nil {
|
||||
for k, v := range v.Params {
|
||||
sb.WriteString(";")
|
||||
sb.WriteString(k)
|
||||
if v == "-1" {
|
||||
//rport 值为-1的时候,没有值
|
||||
continue
|
||||
}
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
v.raw = sb.String()
|
||||
return v.raw
|
||||
}
|
||||
|
||||
//注意via允许以下这种添加空白
|
||||
//Via: SIP / 2.0 / UDP first.example.com: 4000;ttl=16 ;maddr=224.2.0.1 ;branch=z9hG4bKa7c6a8dlze.1
|
||||
//Via: SIP/2.0/UDP 192.168.1.64:5060;rport=5060;received=192.168.1.64;branch=z9hG4bK1000615294
|
||||
func (v *Via) Parse(str string) (err error) {
|
||||
v.raw = str
|
||||
|
||||
str = strings.Trim(str, ";")
|
||||
arr1 := strings.Split(str, ";")
|
||||
part1 := strings.TrimSpace(arr1[0]) //SIP / 2.0 / UDP first.example.com: 4000
|
||||
|
||||
v.Host, v.Port = "", ""
|
||||
if n1 := strings.IndexByte(part1, ':'); n1 != -1 {
|
||||
v.Port = strings.TrimSpace(part1[n1+1:])
|
||||
part1 = strings.TrimSpace(part1[:n1])
|
||||
}
|
||||
|
||||
n2 := strings.LastIndexByte(part1, ' ')
|
||||
if n2 == -1 {
|
||||
v.Host = part1 //error?
|
||||
} else {
|
||||
v.Host = strings.TrimSpace(part1[n2+1:])
|
||||
|
||||
//解析protocol、version和transport,SIP / 2.0 / UDP
|
||||
part2 := part1[:n2]
|
||||
arr2 := strings.Split(part2, "/")
|
||||
if len(arr2) != 3 {
|
||||
err = errors.New("parse contait part1.1 failed:" + part2)
|
||||
return
|
||||
}
|
||||
v.Version = fmt.Sprintf("%s/%s", strings.TrimSpace(arr2[0]), strings.TrimSpace(arr2[1]))
|
||||
v.Transport = strings.TrimSpace(arr2[2])
|
||||
}
|
||||
|
||||
//必须有参数
|
||||
v.Params = make(map[string]string)
|
||||
for i, one := range arr1 {
|
||||
if i == 0 {
|
||||
//arr[0]已经处理
|
||||
continue
|
||||
}
|
||||
one = strings.TrimSpace(one)
|
||||
arr2 := strings.Split(one, "=")
|
||||
//rport 这个参数可能没有 value。 -1:no-value, other-value
|
||||
if len(arr2) == 1 {
|
||||
if arr2[0] == "rport" {
|
||||
v.Params["rport"] = "-1"
|
||||
continue
|
||||
} else {
|
||||
fmt.Println("invalid param:", one)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
k, val := arr2[0], arr2[1]
|
||||
v.Params[k] = val
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
//CSeq: 101 INVITE
|
||||
//CSeq: 2 REGISTER
|
||||
type CSeq struct {
|
||||
raw string //原始内容
|
||||
ID uint32
|
||||
Method Method
|
||||
}
|
||||
|
||||
func (c *CSeq) String() string {
|
||||
c.raw = fmt.Sprintf("%d %s", c.ID, c.Method)
|
||||
return c.raw
|
||||
}
|
||||
|
||||
func (c *CSeq) Parse(str string) error {
|
||||
c.raw = str
|
||||
arr1 := strings.Split(str, " ")
|
||||
n, err := strconv.ParseInt(arr1[0], 10, 64)
|
||||
if err != nil {
|
||||
fmt.Println("parse cseq faield:", str)
|
||||
return err
|
||||
}
|
||||
c.ID = uint32(n)
|
||||
c.Method = Method(arr1[1])
|
||||
return nil
|
||||
}
|
||||
|
||||
//sip:user:password@domain;uri-parameters?headers
|
||||
/*
|
||||
RFC3261
|
||||
SIP-URI = "sip:" [ userinfo ] hostport
|
||||
uri-parameters [ headers ]
|
||||
SIPS-URI = "sips:" [ userinfo ] hostport
|
||||
uri-parameters [ headers ]
|
||||
userinfo = ( user / telephone-subscriber ) [ ":" password ] "@"
|
||||
user = 1*( unreserved / escaped / user-unreserved )
|
||||
user-unreserved = "&" / "=" / "+" / "$" / "," / ";" / "?" / "/"
|
||||
password = *( unreserved / escaped /
|
||||
"&" / "=" / "+" / "$" / "," )
|
||||
hostport = host [ ":" port ]
|
||||
host = hostname / IPv4address / IPv6reference
|
||||
hostname = *( domainlabel "." ) toplabel [ "." ]
|
||||
domainlabel = alphanum
|
||||
/ alphanum *( alphanum / "-" ) alphanum
|
||||
toplabel = ALPHA / ALPHA *( alphanum / "-" ) alphanum
|
||||
|
||||
IPv4address = 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT "." 1*3DIGIT
|
||||
IPv6reference = "[" IPv6address "]"
|
||||
IPv6address = hexpart [ ":" IPv4address ]
|
||||
hexpart = hexseq / hexseq "::" [ hexseq ] / "::" [ hexseq ]
|
||||
hexseq = hex4 *( ":" hex4)
|
||||
hex4 = 1*4HEXDIG
|
||||
port = 1*DIGIT
|
||||
|
||||
uri-parameters = *( ";" uri-parameter)
|
||||
uri-parameter = transport-param / user-param / method-param
|
||||
/ ttl-param / maddr-param / lr-param / other-param
|
||||
transport-param = "transport="
|
||||
( "udp" / "tcp" / "sctp" / "tls"
|
||||
/ other-transport)
|
||||
other-transport = token
|
||||
user-param = "user=" ( "phone" / "ip" / other-user)
|
||||
other-user = token
|
||||
method-param = "method=" Method
|
||||
ttl-param = "ttl=" ttl
|
||||
maddr-param = "maddr=" host
|
||||
lr-param = "lr"
|
||||
other-param = pname [ "=" pvalue ]
|
||||
pname = 1*paramchar
|
||||
pvalue = 1*paramchar
|
||||
paramchar = param-unreserved / unreserved / escaped
|
||||
param-unreserved = "[" / "]" / "/" / ":" / "&" / "+" / "$"
|
||||
|
||||
headers = "?" header *( "&" header )
|
||||
header = hname "=" hvalue
|
||||
hname = 1*( hnv-unreserved / unreserved / escaped )
|
||||
hvalue = *( hnv-unreserved / unreserved / escaped )
|
||||
hnv-unreserved = "[" / "]" / "/" / "?" / ":" / "+" / "$"
|
||||
*/
|
||||
type URI struct {
|
||||
scheme string // sip sips
|
||||
host string // userinfo@domain or userinfo@ip:port
|
||||
method string // uri和method有关?
|
||||
params map[string]string // include branch/maddr/received/ttl/rport
|
||||
headers map[string]string // include branch/maddr/received/ttl/rport
|
||||
}
|
||||
func (u *URI) Host() string {
|
||||
return u.host
|
||||
}
|
||||
func (u *URI) UserInfo() string {
|
||||
return strings.Split(u.host,"@")[0]
|
||||
}
|
||||
func (u *URI) Domain() string {
|
||||
return strings.Split(u.host,"@")[1]
|
||||
}
|
||||
func (u *URI) IP() string {
|
||||
t:=strings.Split(u.host,"@")
|
||||
if len(t) == 1 {
|
||||
return strings.Split(t[0],":")[0]
|
||||
}
|
||||
return strings.Split(t[1],":")[0]
|
||||
}
|
||||
func (u *URI) Port() string {
|
||||
t:=strings.Split(u.host,"@")
|
||||
if len(t) == 1 {
|
||||
return strings.Split(t[0],":")[1]
|
||||
}
|
||||
return strings.Split(t[1],":")[1]
|
||||
}
|
||||
func (u *URI) String() string {
|
||||
if u.scheme == "" {
|
||||
u.scheme = "sip"
|
||||
}
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(u.scheme)
|
||||
sb.WriteString(":")
|
||||
sb.WriteString(u.host)
|
||||
if u.params != nil {
|
||||
for k, v := range u.params {
|
||||
sb.WriteString(";")
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
if u.headers != nil {
|
||||
sb.WriteString("?")
|
||||
for k, v := range u.headers {
|
||||
sb.WriteString("&")
|
||||
sb.WriteString(k)
|
||||
sb.WriteString("=")
|
||||
sb.WriteString(v)
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
//对于gb28181,request-uri 不带参数
|
||||
func NewURI(host string) URI {
|
||||
return URI{
|
||||
scheme: "sip",
|
||||
host: host,
|
||||
}
|
||||
}
|
||||
func parseURI(str string) (ret URI, err error) {
|
||||
ret = URI{}
|
||||
|
||||
//解析scheme
|
||||
str = strings.TrimSpace(str)
|
||||
n1 := strings.IndexByte(str, ':')
|
||||
if n1 == -1 {
|
||||
err = errors.New("invalid sheme")
|
||||
return
|
||||
}
|
||||
ret.scheme = str[:n1]
|
||||
str = str[n1+1:]
|
||||
if len(str) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
//解析host
|
||||
n2 := strings.IndexByte(str, ';')
|
||||
if n2 == -1 {
|
||||
ret.host = str
|
||||
return
|
||||
}
|
||||
ret.host = str[:n2]
|
||||
|
||||
str = str[n2+1:]
|
||||
if len(str) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
//解析params and headers
|
||||
var paramStr, headerStr = "", ""
|
||||
n3 := strings.IndexByte(str, '?')
|
||||
if n3 == -1 {
|
||||
paramStr = str
|
||||
} else {
|
||||
paramStr = str[:n3]
|
||||
headerStr = str[n3+1:]
|
||||
}
|
||||
|
||||
//k1=v1;k2=v2
|
||||
if paramStr != "" {
|
||||
ret.params = make(map[string]string)
|
||||
paramStr = strings.Trim(paramStr, ";")
|
||||
arr1 := strings.Split(paramStr, ";")
|
||||
for _, one := range arr1 {
|
||||
tmp := strings.Split(one, "=")
|
||||
k, v := tmp[0], tmp[1]
|
||||
ret.params[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
//k1=v1&k2=v2
|
||||
if headerStr != "" {
|
||||
ret.headers = make(map[string]string)
|
||||
arr2 := strings.Split(paramStr, "&")
|
||||
for _, one := range arr2 {
|
||||
tmp := strings.Split(one, "=")
|
||||
k, v := tmp[0], tmp[1]
|
||||
ret.headers[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
114
sip/head_test.go
114
sip/head_test.go
@@ -1,114 +0,0 @@
|
||||
package sip
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestContact(t *testing.T) {
|
||||
str1 := "\"Mr.Watson\" <sip:watson@worcester.bell-telephone.com>;q=0.7; expires=3600,\"Mr.Watson\" <mailto:watson@bell-telephone.com>";
|
||||
//str1 := `"Mr.Watson" <sip:watson@worcester.bell-telephone.com>;q=0.7;`
|
||||
c := &Contact{}
|
||||
err := c.Parse(str1)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
fmt.Println("source:", str1)
|
||||
fmt.Println("result:", c.String())
|
||||
}
|
||||
|
||||
func TestVia(t *testing.T) {
|
||||
str1 := "SIP / 2.0 / UDP first.example.com: 4000;ttl=16 ;maddr=224.2.0.1 ;branch=z9hG4bKa7c6a8dlze.1"
|
||||
str2 := "SIP/2.0/UDP 192.168.1.64:5060;rport;received=192.168.1.64;branch=z9hG4bK1000615294"
|
||||
|
||||
var err error
|
||||
v1 := &Via{}
|
||||
err = v1.Parse(str1)
|
||||
if err != nil {
|
||||
fmt.Println("error:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("source:%v\n", str1)
|
||||
fmt.Printf("result:%v\n", v1.String())
|
||||
|
||||
v2 := &Via{}
|
||||
err = v2.Parse(str2)
|
||||
if err != nil {
|
||||
fmt.Println("error:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("source:%v\n", str2)
|
||||
fmt.Printf("result:%v\n", v2.String())
|
||||
|
||||
}
|
||||
|
||||
func TestMessage1(t *testing.T) {
|
||||
str1 := `REGISTER sip:34020000002000000001@3402000000 SIP/2.0
|
||||
Via: SIP/2.0/UDP 192.168.1.64:5060;rport;branch=z9hG4bK385701375
|
||||
From: <sip:34020000001320000001@3402000000>;tag=1840661473
|
||||
To: <sip:34020000001320000001@3402000000>
|
||||
Call-ID: 418133739
|
||||
CSeq: 1 REGISTER
|
||||
Contact: <sip:34020000001320000001@192.168.1.64:5060>
|
||||
Max-Forwards: 70
|
||||
User-Agent: IP Camera
|
||||
Expires: 3600
|
||||
Content-Length: 0`
|
||||
|
||||
fmt.Println("input:")
|
||||
fmt.Println(str1)
|
||||
msg, err := Decode([]byte(str1))
|
||||
if err != nil {
|
||||
fmt.Println("decode message failed:", err.Error())
|
||||
return
|
||||
}
|
||||
out, err := Encode(msg)
|
||||
if err != nil {
|
||||
fmt.Println("encode message failed:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println("=====================================")
|
||||
fmt.Println("output:")
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
|
||||
func TestMessage2(t *testing.T) {
|
||||
str1 := `SIP/2.0 200 OK
|
||||
Via: SIP/2.0/UDP 192.168.1.151:5060;rport=5060;branch=SrsGbB56116414
|
||||
From: <sip:34020000002000000001@3402000000>;tag=SrsGbF72006729
|
||||
To: <sip:34020000001320000001@3402000000>;tag=416442565
|
||||
Call-ID: 202093500940
|
||||
CSeq: 101 INVITE
|
||||
Contact: <sip:34020000001320000001@192.168.1.64:5060>
|
||||
Content-Type: application/sdp
|
||||
User-Agent: IP Camera
|
||||
Content-Length: 185
|
||||
|
||||
v=0
|
||||
o=34020000001320000001 1835 1835 IN IP4 192.168.1.64
|
||||
s=Play
|
||||
c=IN IP4 192.168.1.64
|
||||
t=0 0
|
||||
m=video 15060 RTP/AVP 96
|
||||
a=sendonly
|
||||
a=rtpmap:96 PS/90000
|
||||
a=filesize:0
|
||||
y=0009093131`
|
||||
|
||||
fmt.Println("input:")
|
||||
fmt.Println(str1)
|
||||
msg, err := Decode([]byte(str1))
|
||||
if err != nil {
|
||||
fmt.Println("decode message failed:", err.Error())
|
||||
return
|
||||
}
|
||||
out, err := Encode(msg)
|
||||
if err != nil {
|
||||
fmt.Println("encode message failed:", err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Println("=====================================")
|
||||
fmt.Println("output:")
|
||||
fmt.Println(string(out))
|
||||
}
|
||||
455
sip/message.go
455
sip/message.go
@@ -1,455 +0,0 @@
|
||||
package sip
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Monibuca/plugin-gb28181/utils"
|
||||
)
|
||||
|
||||
//Content-Type: Application/MANSCDP+xml
|
||||
//Content-Type: Application/SDP
|
||||
//Call-ID: 202081530679
|
||||
//Max-Forwards: 70
|
||||
//User-Agent: SRS/4.0.32(Leo)
|
||||
//Subject: 34020000001320000001:0009093128,34020000002000000001:0
|
||||
//Content-Length: 164
|
||||
|
||||
type Message struct {
|
||||
Mode Mode //0:REQUEST, 1:RESPONSE
|
||||
|
||||
StartLine *StartLine
|
||||
Via *Via //Via
|
||||
From *Contact //From
|
||||
To *Contact //To
|
||||
CallID string //Call-ID
|
||||
CSeq *CSeq //CSeq
|
||||
Contact *Contact //Contact
|
||||
Authorization string //Authorization
|
||||
MaxForwards int //Max-Forwards
|
||||
UserAgent string //User-Agent
|
||||
Subject string //Subject
|
||||
ContentType string //Content-Type
|
||||
Expires int //Expires
|
||||
ContentLength int //Content-Length
|
||||
Route *Contact
|
||||
Body string
|
||||
Addr string
|
||||
}
|
||||
|
||||
func (m *Message) BuildResponse(code int) *Message {
|
||||
response := Message{
|
||||
Mode: SIP_MESSAGE_RESPONSE,
|
||||
From: m.From,
|
||||
To: m.To,
|
||||
CallID: m.CallID,
|
||||
CSeq: m.CSeq,
|
||||
Via: m.Via,
|
||||
MaxForwards: m.MaxForwards,
|
||||
StartLine: &StartLine{
|
||||
Code: code,
|
||||
},
|
||||
}
|
||||
return &response
|
||||
}
|
||||
|
||||
//z9hG4bK + 10个随机数字
|
||||
func randBranch() string {
|
||||
return fmt.Sprintf("z9hG4bK%s", utils.RandNumString(8))
|
||||
}
|
||||
|
||||
func BuildMessageRequest(method Method, transport, sipSerial, sipRealm, username, srcIP string, srcPort uint16, expires, cseq int, body string) *Message {
|
||||
server := fmt.Sprintf("%s@%s", sipSerial, sipRealm)
|
||||
client := fmt.Sprintf("%s@%s", username, sipRealm)
|
||||
|
||||
msg := &Message{
|
||||
Mode: SIP_MESSAGE_REQUEST,
|
||||
MaxForwards: 70,
|
||||
UserAgent: "IPC",
|
||||
Expires: expires,
|
||||
ContentLength: 0,
|
||||
}
|
||||
msg.StartLine = &StartLine{
|
||||
Method: method,
|
||||
Uri: NewURI(server),
|
||||
}
|
||||
msg.Via = &Via{
|
||||
Transport: transport,
|
||||
Host: client,
|
||||
}
|
||||
msg.Via.Params = map[string]string{
|
||||
"branch": randBranch(),
|
||||
"rport": "-1", //only key,no-value
|
||||
}
|
||||
msg.From = &Contact{
|
||||
Uri: NewURI(client),
|
||||
Params: nil,
|
||||
}
|
||||
msg.From.Params = map[string]string{
|
||||
"tag": utils.RandNumString(10),
|
||||
}
|
||||
msg.To = &Contact{
|
||||
Uri: NewURI(client),
|
||||
}
|
||||
msg.CallID = utils.RandNumString(8)
|
||||
msg.CSeq = &CSeq{
|
||||
ID: uint32(cseq),
|
||||
Method: method,
|
||||
}
|
||||
|
||||
msg.Contact = &Contact{
|
||||
Uri: NewURI(fmt.Sprintf("%s@%s:%d", username, srcIP, srcPort)),
|
||||
}
|
||||
if len(body) > 0 {
|
||||
msg.ContentLength = len(body)
|
||||
msg.Body = body
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
func (m *Message) GetMode() Mode {
|
||||
return m.Mode
|
||||
}
|
||||
|
||||
func (m *Message) IsRequest() bool {
|
||||
return m.Mode == SIP_MESSAGE_REQUEST
|
||||
}
|
||||
|
||||
func (m *Message) IsResponse() bool {
|
||||
return m.Mode == SIP_MESSAGE_RESPONSE
|
||||
}
|
||||
|
||||
func (m *Message) GetMethod() Method {
|
||||
if m.CSeq == nil {
|
||||
b, _ := Encode(m)
|
||||
println(string(b))
|
||||
return MESSAGE
|
||||
}
|
||||
return m.CSeq.Method
|
||||
}
|
||||
|
||||
//此消息是否使用可靠传输
|
||||
func (m *Message) IsReliable() bool {
|
||||
protocol := strings.ToUpper(m.Via.Transport)
|
||||
return "TCP" == protocol || "TLS" == protocol || "SCTP" == protocol
|
||||
}
|
||||
|
||||
//response code
|
||||
func (m *Message) GetStatusCode() int {
|
||||
return m.StartLine.Code
|
||||
}
|
||||
|
||||
//response code and reason
|
||||
func (m *Message) GetReason() string {
|
||||
return DumpError(m.StartLine.Code)
|
||||
}
|
||||
|
||||
func (m *Message) GetBranch() string {
|
||||
if m.Via == nil {
|
||||
panic("invalid via")
|
||||
}
|
||||
if m.Via.Params == nil {
|
||||
panic("invalid via params")
|
||||
}
|
||||
|
||||
b, ok := m.Via.Params["branch"]
|
||||
if !ok {
|
||||
panic("invalid via paramas branch")
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
//构建响应消息的时候,会使用请求消息的 source 和 destination
|
||||
//请求消息的source,格式: host:port
|
||||
func (m *Message) Source() string {
|
||||
if m.Mode == SIP_MESSAGE_RESPONSE {
|
||||
fmt.Println("only for request message")
|
||||
return ""
|
||||
}
|
||||
|
||||
if m.Via == nil {
|
||||
fmt.Println("invalid request message")
|
||||
return ""
|
||||
}
|
||||
|
||||
var (
|
||||
host, port string
|
||||
via = m.Via
|
||||
)
|
||||
|
||||
if received, ok := via.Params["received"]; ok && received != "" {
|
||||
host = received
|
||||
} else {
|
||||
host = via.Host
|
||||
}
|
||||
|
||||
if rport, ok := via.Params["rport"]; ok && rport != "-1" && rport != "0" && rport != "" {
|
||||
port = rport
|
||||
} else if via.Port != "" {
|
||||
port = via.Port
|
||||
} else {
|
||||
//如果port为空,则上层构建消息的时候,根据sip服务的默认端口来选择
|
||||
}
|
||||
return fmt.Sprintf("%v:%v", host, port)
|
||||
}
|
||||
|
||||
//目标地址:这个应该是用于通过route头域实现proxy这样的功能,暂时不支持
|
||||
func (m *Message) Destination() string {
|
||||
//TODO:
|
||||
return ""
|
||||
}
|
||||
|
||||
//=======================================================================================================
|
||||
|
||||
func Decode(data []byte) (msg *Message, err error) {
|
||||
msg = &Message{}
|
||||
|
||||
content := string(data)
|
||||
content = strings.Trim(content, CRLFCRLF)
|
||||
msgArr := strings.Split(content, CRLFCRLF)
|
||||
//第一部分:header
|
||||
//第二部分:body
|
||||
if len(msgArr) == 0 {
|
||||
fmt.Println("invalid sip message:", data)
|
||||
err = errors.New("invalid sip message")
|
||||
return
|
||||
}
|
||||
|
||||
headStr := strings.TrimSpace(msgArr[0])
|
||||
if len(msgArr) > 1 {
|
||||
msg.Body = strings.TrimSpace(msgArr[1])
|
||||
}
|
||||
|
||||
headStr = strings.Trim(headStr, CRLF)
|
||||
headArr := strings.Split(headStr, CRLF)
|
||||
for i, line := range headArr {
|
||||
//fmt.Printf("%02d --- %s ---- %d\n", i, line, len(line))
|
||||
if i == 0 {
|
||||
firstline := strings.Trim(line, " ")
|
||||
tmp := strings.Split(firstline, " ")
|
||||
//if len(tmp) != 3 {
|
||||
// fmt.Println("parse first line failed:", firstline)
|
||||
// err = errors.New("invalid first line")
|
||||
// return
|
||||
//}
|
||||
|
||||
if strings.HasPrefix(firstline, VERSION) {
|
||||
//status line
|
||||
//SIP/2.0 200 OK
|
||||
var num int64
|
||||
num, err = strconv.ParseInt(tmp[1], 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
msg.Mode = SIP_MESSAGE_RESPONSE
|
||||
msg.StartLine = &StartLine{
|
||||
raw: firstline,
|
||||
Version: VERSION,
|
||||
Code: int(num),
|
||||
phrase: strings.Join(tmp[2:], " "),
|
||||
}
|
||||
} else {
|
||||
//request line
|
||||
//REGISTER sip:34020000002000000001@3402000000 SIP/2.0
|
||||
//MESSAGE sip:34020000002000000001@3402000000 SIP/2.0
|
||||
msg.Mode = SIP_MESSAGE_REQUEST
|
||||
msg.StartLine = &StartLine{
|
||||
raw: firstline,
|
||||
Method: Method(tmp[0]),
|
||||
Version: VERSION,
|
||||
}
|
||||
if len(tmp) > 1 {
|
||||
msg.StartLine.Uri, err = parseURI(tmp[1])
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Println(firstline)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pos := strings.IndexByte(line, ':')
|
||||
if pos == -1 {
|
||||
continue
|
||||
}
|
||||
k := strings.ToLower(strings.TrimSpace(line[:pos]))
|
||||
v := strings.TrimSpace(line[pos+1:])
|
||||
//fmt.Printf("%02d ---k = %s , v = %s\n", i, k, v)
|
||||
|
||||
if len(v) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
switch k {
|
||||
case "via":
|
||||
//Via: SIP/2.0/UDP 192.168.1.64:5060;rport;branch=z9hG4bK385701375
|
||||
msg.Via = &Via{}
|
||||
err = msg.Via.Parse(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "from":
|
||||
msg.From = &Contact{}
|
||||
err = msg.From.Parse(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "to":
|
||||
msg.To = &Contact{}
|
||||
err = msg.To.Parse(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "call-id":
|
||||
msg.CallID = v
|
||||
|
||||
case "cseq":
|
||||
//CSeq: 2 REGISTER
|
||||
msg.CSeq = &CSeq{}
|
||||
err = msg.CSeq.Parse(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "contact":
|
||||
msg.Contact = &Contact{}
|
||||
err = msg.Contact.Parse(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
case "max-forwards":
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("parse head faield: %s,%s\n", k, v)
|
||||
return nil, err
|
||||
}
|
||||
msg.MaxForwards = int(n)
|
||||
|
||||
case "user-agent":
|
||||
msg.UserAgent = v
|
||||
|
||||
case "expires":
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("parse head faield: %s,%s\n", k, v)
|
||||
return nil, err
|
||||
}
|
||||
msg.Expires = int(n)
|
||||
|
||||
case "content-length":
|
||||
n, err := strconv.ParseInt(v, 10, 64)
|
||||
if err != nil {
|
||||
fmt.Printf("parse head faield: %s,%s\n", k, v)
|
||||
return nil, err
|
||||
}
|
||||
msg.ContentLength = int(n)
|
||||
|
||||
case "authorization":
|
||||
msg.Authorization = v
|
||||
|
||||
case "content-type":
|
||||
msg.ContentType = v
|
||||
case "route":
|
||||
//msg.Route = new(Contact)
|
||||
//msg.Route.Parse(v)
|
||||
default:
|
||||
fmt.Printf("invalid sip head: %s,%s\n", k, v)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func Encode(msg *Message) ([]byte, error) {
|
||||
sb := strings.Builder{}
|
||||
sb.WriteString(msg.StartLine.String())
|
||||
sb.WriteString(CRLF)
|
||||
|
||||
if msg.Via != nil {
|
||||
sb.WriteString("Via: ")
|
||||
sb.WriteString(msg.Via.String())
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.From != nil {
|
||||
sb.WriteString("From: ")
|
||||
sb.WriteString(msg.From.String())
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.To != nil {
|
||||
sb.WriteString("To: ")
|
||||
sb.WriteString(msg.To.String())
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.CallID != "" {
|
||||
sb.WriteString("Call-ID: ")
|
||||
sb.WriteString(msg.CallID)
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
if msg.CSeq != nil {
|
||||
sb.WriteString("CSeq: ")
|
||||
sb.WriteString(msg.CSeq.String())
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.Contact != nil {
|
||||
sb.WriteString("Contact: ")
|
||||
sb.WriteString(msg.Contact.String())
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.UserAgent != "" {
|
||||
sb.WriteString("User-Agent: ")
|
||||
sb.WriteString(msg.UserAgent)
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.ContentType != "" {
|
||||
sb.WriteString("Content-Type: ")
|
||||
sb.WriteString(msg.ContentType)
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.Expires != 0 {
|
||||
sb.WriteString("Expires: ")
|
||||
sb.WriteString(strconv.Itoa(msg.Expires))
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
|
||||
if msg.IsRequest() {
|
||||
//request only
|
||||
|
||||
sb.WriteString("Max-Forwards: ")
|
||||
sb.WriteString(strconv.Itoa(msg.MaxForwards))
|
||||
sb.WriteString(CRLF)
|
||||
|
||||
if msg.Authorization != "" {
|
||||
sb.WriteString("Authorization: ")
|
||||
sb.WriteString(msg.Authorization)
|
||||
sb.WriteString(CRLF)
|
||||
}
|
||||
} else {
|
||||
//response only
|
||||
}
|
||||
|
||||
sb.WriteString("Content-Length: ")
|
||||
sb.WriteString(strconv.Itoa(msg.ContentLength))
|
||||
|
||||
sb.WriteString(CRLFCRLF)
|
||||
|
||||
if msg.Body != "" {
|
||||
sb.WriteString(msg.Body)
|
||||
}
|
||||
|
||||
return []byte(sb.String()), nil
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
#### 事物
|
||||
transaction事务是SIP的基本组成部分。
|
||||
一个事务是客户发送的一个请求事务(通过通讯层)发送到一个服务器事务,连同服务器事务的所有的该请求的应答发送回客户端事务。
|
||||
事务层处理应用服务层的重发,匹配请求的应答,以及应用服务层的超时。
|
||||
任何一个用户代理客户端(user agent client UAC)完成的事情都是由一组事务构成的。
|
||||
用户代理包含一个事务层,来实现有状态的代理服务器。
|
||||
事务层包含一个客户元素(可以认为是一个客户事务)和一个服务器元素(可以认为是一个服务器事务),他们都可以用一个有限状态机来处理特定的请求。
|
||||
|
||||
在状态机层面,事物分为ict、ist、nict、nist四种。
|
||||
但底层事物方面,仅根据 method 等信息,分支处理。
|
||||
|
||||
#### 关于事物管理
|
||||
|
||||
事物在map里面管理,事物ID的选择是要和事物匹配相关。
|
||||
|
||||
```
|
||||
客户端事件的匹配
|
||||
|
||||
当客户端中的传输层收到响应时,它必须确定哪个客户端事务将处理该响应,以便可以进行17.1.1和17.1.2节的处理。头域 Via 字段中的branch参数用于匹配规则。 一个匹配的响应应该满足下面两个条件:
|
||||
1.如果响应的头域 Via头字段中的branch参数值与创建事务的请求的头域 Via头字段中的branch参数值相同。
|
||||
2.如果CSeq标头字段中的method参数与创建事务的请求的方法匹配。(由于CANCEL请求会创建新的事务,但共享相同的branch参数值。所以仅用branch参数是不够的)
|
||||
|
||||
|
||||
服务端事物匹配
|
||||
|
||||
首先要检查请求中的Via头域的branch参数。如果他以”z9hG4bk”开头,那么这个请求一定是由客户端事务根据本规范产生的。因此,branch参数在该客户端发出的所有的事务中都是唯一的。根据下列规则我们可以判定请求是否和事务匹配:
|
||||
|
||||
1、 请求的Via头域的branch参数和创建本事务的请求的Via头域的branch参数一样,并且:
|
||||
2、 请求的Via头域的send-by参数和创建本事务的请求的Via头域的send-by参数一样(可能存在来自不同客户端的branch参数的意外或恶意重复,所以将 send-by 值用作匹配的一部分),并且:
|
||||
3、 请求的方法与创建事务的方法匹配,但ACK除外,在ACK中,创建事务的请求的方法为INVITE。
|
||||
```
|
||||
|
||||
所以,根据匹配规则,事物的ID 使用 branch,然后在匹配逻辑里面,再做条件判断。而因为branch可能会重复,所以如果使用map来简化transaction的管理,key的取值应该:
|
||||
|
||||
客户端事物: branch+method
|
||||
服务端事物: branch + sendby + method,method中ack还要除外。所以只能用branch + sendby
|
||||
@@ -1,60 +0,0 @@
|
||||
package transaction
|
||||
|
||||
//SIP服务器静态配置信息
|
||||
|
||||
/*
|
||||
# sip监听udp端口
|
||||
listen 5060;
|
||||
|
||||
# SIP server ID(SIP服务器ID).
|
||||
# 设备端配置编号需要与该值一致,否则无法注册
|
||||
serial 34020000002000000001;
|
||||
|
||||
# SIP server domain(SIP服务器域)
|
||||
realm 3402000000;
|
||||
|
||||
# 服务端发送ack后,接收回应的超时时间,单位为秒
|
||||
# 如果指定时间没有回应,认为失败
|
||||
ack_timeout 30;
|
||||
|
||||
# 设备心跳维持时间,如果指定时间内(秒)没有接收一个心跳
|
||||
# 认为设备离线
|
||||
keepalive_timeout 120;
|
||||
|
||||
# 注册之后是否自动给设备端发送invite
|
||||
# on: 是 off 不是,需要通过api控制
|
||||
auto_play on;
|
||||
# 设备将流发送的端口,是否固定
|
||||
# on 发送流到多路复用端口 如9000
|
||||
# off 自动从rtp_mix_port - rtp_max_port 之间的值中
|
||||
# 选一个可以用的端口
|
||||
invite_port_fixed on;
|
||||
|
||||
# 向设备或下级域查询设备列表的间隔,单位(秒)
|
||||
# 默认60秒
|
||||
query_catalog_interval 60;
|
||||
*/
|
||||
|
||||
type Config struct {
|
||||
//sip服务器的配置
|
||||
SipNetwork string //传输协议,默认UDP,可选TCP
|
||||
SipIP string //sip 服务器公网IP
|
||||
SipPort uint16 //sip 服务器端口,默认 5060
|
||||
Serial string //sip 服务器 id, 默认 34020000002000000001
|
||||
Realm string //sip 服务器域,默认 3402000000
|
||||
|
||||
AckTimeout uint16 //sip 服务应答超时,单位秒
|
||||
RegisterValidity int //注册有效期,单位秒,默认 3600
|
||||
RegisterInterval int //注册间隔,单位秒,默认 60
|
||||
HeartbeatInterval int //心跳间隔,单位秒,默认 60
|
||||
HeartbeatRetry int //心跳超时次数,默认 3
|
||||
|
||||
//媒体服务器配置
|
||||
MediaIP string //媒体服务器地址
|
||||
MediaPort uint16 //媒体服务器端口
|
||||
MediaPortMin uint16
|
||||
MediaPortMax uint16
|
||||
MediaIdleTimeout uint16 //推流超时时间,超过则断开链接,让设备重连
|
||||
AudioEnable bool //是否开启音频
|
||||
WaitKeyFrame bool //是否等待关键帧,如果等待,则在收到第一个关键帧之前,忽略所有媒体流
|
||||
}
|
||||
@@ -1,463 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"regexp"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"github.com/Monibuca/plugin-gb28181/transport"
|
||||
"github.com/Monibuca/plugin-gb28181/utils"
|
||||
)
|
||||
|
||||
//Core: transactions manager
|
||||
//管理所有 transactions,以及相关全局参数、运行状态机
|
||||
type Core struct {
|
||||
ctx context.Context //上下文
|
||||
handlers map[State]map[Event]Handler //每个状态都可以处理有限个事件。不必加锁。
|
||||
transactions map[string]*Transaction //管理所有 transactions,key:tid,value:transaction
|
||||
mutex sync.Mutex //transactions的锁
|
||||
removeTa chan string //要删除transaction的时候,通过chan传递tid
|
||||
tp transport.ITransport //transport
|
||||
config *Config //sip server配置信息
|
||||
Devices sync.Map
|
||||
OnInvite func(*Channel) int
|
||||
}
|
||||
|
||||
var xmlReg *regexp.Regexp
|
||||
|
||||
func init() {
|
||||
xmlReg, _ = regexp.Compile("<\\?.+\\?>")
|
||||
}
|
||||
|
||||
//初始化一个 Core,需要能响应请求,也要能发起请求
|
||||
//client 发起请求
|
||||
//server 响应请求
|
||||
//TODO:根据角色,增加相关配置信息
|
||||
//TODO:通过context管理子线程
|
||||
//TODO:单元测试
|
||||
func NewCore(config *Config) *Core {
|
||||
core := &Core{
|
||||
handlers: make(map[State]map[Event]Handler),
|
||||
transactions: make(map[string]*Transaction),
|
||||
removeTa: make(chan string, 10),
|
||||
config: config,
|
||||
ctx: context.Background(),
|
||||
}
|
||||
if config.SipNetwork == "TCP" {
|
||||
core.tp = transport.NewTCPServer(config.SipPort, true)
|
||||
} else {
|
||||
core.tp = transport.NewUDPServer(config.SipPort)
|
||||
}
|
||||
//填充fsm
|
||||
core.addICTHandler()
|
||||
core.addISTHandler()
|
||||
core.addNICTHandler()
|
||||
core.addNISTHandler()
|
||||
return core
|
||||
}
|
||||
|
||||
//add transaction to core
|
||||
func (c *Core) AddTransaction(ta *Transaction) {
|
||||
c.mutex.Lock()
|
||||
c.transactions[ta.id] = ta
|
||||
c.mutex.Unlock()
|
||||
go ta.Run()
|
||||
}
|
||||
|
||||
//delete transaction
|
||||
func (c *Core) DelTransaction(tid string) {
|
||||
c.mutex.Lock()
|
||||
delete(c.transactions, tid)
|
||||
c.mutex.Unlock()
|
||||
}
|
||||
|
||||
//创建事件:根据接收到的消息创建消息事件
|
||||
func (c *Core) NewInComingMessageEvent(m *sip.Message) *EventObj {
|
||||
return &EventObj{
|
||||
evt: getInComingMessageEvent(m),
|
||||
tid: getMessageTransactionID(m),
|
||||
msg: m,
|
||||
}
|
||||
}
|
||||
|
||||
//创建事件:根据发出的消息创建消息事件
|
||||
func (c *Core) NewOutGoingMessageEvent(m *sip.Message) *EventObj {
|
||||
return &EventObj{
|
||||
evt: getOutGoingMessageEvent(m),
|
||||
tid: getMessageTransactionID(m),
|
||||
msg: m,
|
||||
}
|
||||
}
|
||||
|
||||
//创建事物
|
||||
//填充此事物的参数:via、from、to、callID、cseq
|
||||
func (c *Core) initTransaction(ctx context.Context, obj *EventObj) *Transaction {
|
||||
m := obj.msg
|
||||
|
||||
//ack要么属于一个invite事物,要么由TU层直接管理,不通过事物管理。
|
||||
if m.GetMethod() == sip.ACK {
|
||||
fmt.Println("ack nerver create transaction")
|
||||
return nil
|
||||
}
|
||||
ta := &Transaction{
|
||||
id: obj.tid,
|
||||
core: c,
|
||||
ctx: ctx,
|
||||
done: make(chan struct{}),
|
||||
event: make(chan *EventObj, 10), //带缓冲的event channel
|
||||
response: make(chan *Response),
|
||||
startAt: time.Now(),
|
||||
endAt: time.Now().Add(1000000 * time.Hour),
|
||||
}
|
||||
//填充其他transaction的信息
|
||||
ta.via = m.Via
|
||||
ta.from = m.From
|
||||
ta.to = m.To
|
||||
ta.callID = m.CallID
|
||||
ta.cseq = m.CSeq
|
||||
ta.origRequest = m
|
||||
|
||||
return ta
|
||||
}
|
||||
|
||||
//状态机初始化:ICT
|
||||
func (c *Core) addICTHandler() {
|
||||
c.addHandler(ICT_PRE_CALLING, SND_REQINVITE, ict_snd_invite)
|
||||
c.addHandler(ICT_CALLING, TIMEOUT_A, osip_ict_timeout_a_event)
|
||||
c.addHandler(ICT_CALLING, TIMEOUT_B, osip_ict_timeout_b_event)
|
||||
c.addHandler(ICT_CALLING, RCV_STATUS_1XX, ict_rcv_1xx)
|
||||
c.addHandler(ICT_CALLING, RCV_STATUS_2XX, ict_rcv_2xx)
|
||||
c.addHandler(ICT_CALLING, RCV_STATUS_3456XX, ict_rcv_3456xx)
|
||||
c.addHandler(ICT_PROCEEDING, RCV_STATUS_1XX, ict_rcv_1xx)
|
||||
c.addHandler(ICT_PROCEEDING, RCV_STATUS_2XX, ict_rcv_2xx)
|
||||
c.addHandler(ICT_PROCEEDING, RCV_STATUS_3456XX, ict_rcv_3456xx)
|
||||
c.addHandler(ICT_COMPLETED, RCV_STATUS_3456XX, ict_retransmit_ack)
|
||||
c.addHandler(ICT_COMPLETED, TIMEOUT_D, osip_ict_timeout_d_event)
|
||||
}
|
||||
|
||||
//状态机初始化:IST
|
||||
func (c *Core) addISTHandler() {
|
||||
c.addHandler(IST_PRE_PROCEEDING, RCV_REQINVITE, ist_rcv_invite)
|
||||
c.addHandler(IST_PROCEEDING, RCV_REQINVITE, ist_rcv_invite)
|
||||
c.addHandler(IST_COMPLETED, RCV_REQINVITE, ist_rcv_invite)
|
||||
c.addHandler(IST_COMPLETED, TIMEOUT_G, osip_ist_timeout_g_event)
|
||||
c.addHandler(IST_COMPLETED, TIMEOUT_H, osip_ist_timeout_h_event)
|
||||
c.addHandler(IST_PROCEEDING, SND_STATUS_1XX, ist_snd_1xx)
|
||||
c.addHandler(IST_PROCEEDING, SND_STATUS_2XX, ist_snd_2xx)
|
||||
c.addHandler(IST_PROCEEDING, SND_STATUS_3456XX, ist_snd_3456xx)
|
||||
c.addHandler(IST_COMPLETED, RCV_REQACK, ist_rcv_ack)
|
||||
c.addHandler(IST_CONFIRMED, RCV_REQACK, ist_rcv_ack)
|
||||
c.addHandler(IST_CONFIRMED, TIMEOUT_I, osip_ist_timeout_i_event)
|
||||
}
|
||||
|
||||
//状态机初始化:NICT
|
||||
func (c *Core) addNICTHandler() {
|
||||
c.addHandler(NICT_PRE_TRYING, SND_REQUEST, nict_snd_request)
|
||||
c.addHandler(NICT_TRYING, TIMEOUT_F, osip_nict_timeout_f_event)
|
||||
c.addHandler(NICT_TRYING, TIMEOUT_E, osip_nict_timeout_e_event)
|
||||
c.addHandler(NICT_TRYING, RCV_STATUS_1XX, nict_rcv_1xx)
|
||||
c.addHandler(NICT_TRYING, RCV_STATUS_2XX, nict_rcv_23456xx)
|
||||
c.addHandler(NICT_TRYING, RCV_STATUS_3456XX, nict_rcv_23456xx)
|
||||
c.addHandler(NICT_PROCEEDING, TIMEOUT_F, osip_nict_timeout_f_event)
|
||||
c.addHandler(NICT_PROCEEDING, TIMEOUT_E, osip_nict_timeout_e_event)
|
||||
c.addHandler(NICT_PROCEEDING, RCV_STATUS_1XX, nict_rcv_1xx)
|
||||
c.addHandler(NICT_PROCEEDING, RCV_STATUS_2XX, nict_rcv_23456xx)
|
||||
c.addHandler(NICT_PROCEEDING, RCV_STATUS_3456XX, nict_rcv_23456xx)
|
||||
c.addHandler(NICT_COMPLETED, TIMEOUT_K, osip_nict_timeout_k_event)
|
||||
}
|
||||
|
||||
//状态机初始化:NIST
|
||||
func (c *Core) addNISTHandler() {
|
||||
c.addHandler(NIST_PRE_TRYING, RCV_REQUEST, nist_rcv_request)
|
||||
c.addHandler(NIST_TRYING, SND_STATUS_1XX, nist_snd_1xx)
|
||||
c.addHandler(NIST_TRYING, SND_STATUS_2XX, nist_snd_23456xx)
|
||||
c.addHandler(NIST_TRYING, SND_STATUS_3456XX, nist_snd_23456xx)
|
||||
c.addHandler(NIST_PROCEEDING, SND_STATUS_1XX, nist_snd_1xx)
|
||||
c.addHandler(NIST_PROCEEDING, SND_STATUS_2XX, nist_snd_23456xx)
|
||||
c.addHandler(NIST_PROCEEDING, SND_STATUS_3456XX, nist_snd_23456xx)
|
||||
c.addHandler(NIST_PROCEEDING, RCV_REQUEST, nist_rcv_request)
|
||||
c.addHandler(NIST_COMPLETED, TIMEOUT_J, osip_nist_timeout_j_event)
|
||||
c.addHandler(NIST_COMPLETED, RCV_REQUEST, nist_rcv_request)
|
||||
}
|
||||
|
||||
//状态机初始化:根据state 匹配到对应的状态机
|
||||
func (c *Core) addHandler(state State, event Event, handler Handler) {
|
||||
m := c.handlers
|
||||
|
||||
if state >= DIALOG_CLOSE {
|
||||
fmt.Println("invalid state:", state)
|
||||
return
|
||||
}
|
||||
|
||||
if event >= UNKNOWN_EVT {
|
||||
fmt.Println("invalid event:", event)
|
||||
return
|
||||
}
|
||||
|
||||
if _, ok := m[state]; !ok {
|
||||
m[state] = make(map[Event]Handler)
|
||||
}
|
||||
|
||||
if _, ok := m[state][event]; ok {
|
||||
fmt.Printf("state:%d,event:%d, has been exist\n", state, event)
|
||||
} else {
|
||||
m[state][event] = handler
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Core) Start() {
|
||||
go c.Handler()
|
||||
|
||||
c.tp.Start()
|
||||
}
|
||||
|
||||
func (c *Core) Handler() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("packet handler panic: ", err)
|
||||
utils.PrintStack()
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
ch := c.tp.ReadPacketChan()
|
||||
//阻塞读取消息
|
||||
for {
|
||||
//fmt.Println("PacketHandler ========== SIP Client")
|
||||
select {
|
||||
case tid := <-c.removeTa:
|
||||
c.DelTransaction(tid)
|
||||
case p := <-ch:
|
||||
err := c.HandleReceiveMessage(p)
|
||||
if err != nil {
|
||||
fmt.Println("handler sip response message failed:", err.Error())
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//发送消息:发送请求或者响应
|
||||
//发送消息仅负责发送。报错有两种:1、发送错误。2、发送了但是超时没有收到响应
|
||||
//如果发送成功,如何判断是否收到响应?没有收到响应要重传
|
||||
//所以一个transaction 有read和wriet的chan。
|
||||
//发送的时候写 write chan
|
||||
//接收的时候读取 read chan
|
||||
//发送之后,就开启timer,超时重传,还要记录和修改每次超时时间。不超时的话,记得删掉timer
|
||||
//发送 register 消息
|
||||
func (c *Core) SendMessage(msg *sip.Message) *Response {
|
||||
methond := msg.GetMethod()
|
||||
fmt.Println("send message:", methond)
|
||||
|
||||
e := c.NewOutGoingMessageEvent(msg)
|
||||
|
||||
//匹配事物
|
||||
ta, ok := c.transactions[e.tid]
|
||||
if !ok {
|
||||
//新的请求
|
||||
ta = c.initTransaction(c.ctx, e)
|
||||
|
||||
//如果是sip 消息事件,则将消息缓存,填充typo和state
|
||||
if msg.IsRequest() {
|
||||
//as uac
|
||||
if msg.GetMethod() == sip.INVITE || msg.GetMethod() == sip.ACK {
|
||||
ta.typo = FSM_ICT
|
||||
ta.state = ICT_PRE_CALLING
|
||||
} else {
|
||||
ta.typo = FSM_NICT
|
||||
ta.state = NICT_PRE_TRYING
|
||||
}
|
||||
} else {
|
||||
//as uas:send response
|
||||
|
||||
}
|
||||
|
||||
c.AddTransaction(ta)
|
||||
}
|
||||
|
||||
//把event推到transaction
|
||||
ta.event <- e
|
||||
|
||||
//等待事件结束,并返回
|
||||
return <-ta.response
|
||||
}
|
||||
|
||||
//接收到的消息处理
|
||||
//收到消息有两种:1、请求消息 2、响应消息
|
||||
//请求消息则直接响应处理。
|
||||
//响应消息则需要匹配到请求,让请求的transaction来处理。
|
||||
//TODO:参考srs和osip的流程,以及文档,做最终处理。需要将逻辑分成两层:TU 层和 transaction 层
|
||||
func (c *Core) HandleReceiveMessage(p *transport.Packet) (err error) {
|
||||
//fmt.Println("packet content:", string(p.Data))
|
||||
var msg *sip.Message
|
||||
msg, err = sip.Decode(p.Data)
|
||||
if err != nil {
|
||||
fmt.Println("parse sip message failed:", err.Error())
|
||||
return ErrorParse
|
||||
}
|
||||
if msg.Via == nil {
|
||||
return ErrorParse
|
||||
}
|
||||
//这里不处理超过MTU的包,不处理半包
|
||||
err = checkMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
//fmt.Println("receive message:", msg.GetMethod())
|
||||
|
||||
e := c.NewInComingMessageEvent(msg)
|
||||
|
||||
//一般应该是uas对于接收到的request做预处理
|
||||
if msg.IsRequest() {
|
||||
fixReceiveMessageViaParams(msg, p.Addr)
|
||||
} else {
|
||||
//TODO:对于uac,收到response消息,是否要检查 rport 和 received 呢?因为uas可能对此做了修改
|
||||
}
|
||||
//TODO:CANCEL、BYE 和 ACK 需要特殊处理,使用事物或者直接由TU层处理
|
||||
//查找transaction
|
||||
ta, ok := c.transactions[e.tid]
|
||||
|
||||
method := msg.GetMethod()
|
||||
if msg.IsRequest() {
|
||||
switch method {
|
||||
case sip.ACK:
|
||||
//TODO:this should be a ACK for 2xx (but could be a late ACK!)
|
||||
return
|
||||
case sip.BYE:
|
||||
c.Send(msg.BuildResponse(200))
|
||||
return
|
||||
case sip.MESSAGE:
|
||||
if v, ok := c.Devices.Load(msg.From.Uri.UserInfo()); ok {
|
||||
d := v.(*Device)
|
||||
if d.Status == string(sip.REGISTER) {
|
||||
d.Status = "ONLINE"
|
||||
}
|
||||
d.UpdateTime = time.Now()
|
||||
temp := &struct {
|
||||
XMLName xml.Name
|
||||
CmdType string
|
||||
}{}
|
||||
msg.Body = xmlReg.ReplaceAllString(msg.Body, "")
|
||||
xml.Unmarshal([]byte(msg.Body), temp)
|
||||
switch temp.XMLName.Local {
|
||||
case "Notify":
|
||||
go d.Query()
|
||||
case "Response":
|
||||
switch temp.CmdType {
|
||||
case "Catalog":
|
||||
temp := &struct {
|
||||
DeviceList []Channel `xml:"DeviceList>Item"`
|
||||
}{}
|
||||
xml.Unmarshal([]byte(msg.Body), temp)
|
||||
d.UpdateChannels(temp.DeviceList)
|
||||
}
|
||||
}
|
||||
if ta == nil {
|
||||
c.Send(msg.BuildResponse(200))
|
||||
}
|
||||
}
|
||||
if ta != nil {
|
||||
ta.event <- c.NewOutGoingMessageEvent(msg.BuildResponse(200))
|
||||
}
|
||||
case sip.REGISTER:
|
||||
if !ok {
|
||||
ta = c.initTransaction(c.ctx, e)
|
||||
ta.typo = FSM_NIST
|
||||
ta.state = NIST_PROCEEDING
|
||||
c.AddTransaction(ta)
|
||||
}
|
||||
c.AddDevice(msg)
|
||||
ta.event <- c.NewOutGoingMessageEvent(msg.BuildResponse(200))
|
||||
//case sip.INVITE:
|
||||
// ta.typo = FSM_IST
|
||||
// ta.state = IST_PRE_PROCEEDING
|
||||
case sip.CANCEL:
|
||||
//TODO:CANCEL处理
|
||||
/* special handling for CANCEL */
|
||||
/* in the new spec, if the CANCEL has a Via branch, then it
|
||||
is the same as the one in the original INVITE */
|
||||
return
|
||||
}
|
||||
} else if ok {
|
||||
ta.event <- e
|
||||
if msg.GetStatusCode() >= 200 {
|
||||
ta.response <- &Response{
|
||||
Code: msg.GetStatusCode(),
|
||||
Data: msg,
|
||||
Message: msg.GetReason(),
|
||||
}
|
||||
}
|
||||
}
|
||||
//TODO:TU层处理:根据需要,创建,或者匹配 Dialog
|
||||
//通过tag匹配到call和dialog
|
||||
//处理是否要重传ack
|
||||
return
|
||||
}
|
||||
func (c *Core) Send(msg *sip.Message) error {
|
||||
addr := msg.Addr
|
||||
|
||||
if addr == "" {
|
||||
viaParams := msg.Via.Params
|
||||
var host, port string
|
||||
var ok1, ok2 bool
|
||||
if host, ok1 = viaParams["maddr"]; !ok1 {
|
||||
if host, ok2 = viaParams["received"]; !ok2 {
|
||||
host = msg.Via.Host
|
||||
}
|
||||
}
|
||||
//port
|
||||
port = viaParams["rport"]
|
||||
if port == "" || port == "0" || port == "-1" {
|
||||
port = msg.Via.Port
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
port = "5060"
|
||||
}
|
||||
|
||||
addr = fmt.Sprintf("%s:%s", host, port)
|
||||
}
|
||||
|
||||
fmt.Println("dest addr:", addr)
|
||||
var err1, err2 error
|
||||
pkt := &transport.Packet{}
|
||||
pkt.Data, err1 = sip.Encode(msg)
|
||||
|
||||
if msg.Via.Transport == "UDP" {
|
||||
pkt.Addr, err2 = net.ResolveUDPAddr("udp", addr)
|
||||
} else {
|
||||
pkt.Addr, err2 = net.ResolveTCPAddr("tcp", addr)
|
||||
}
|
||||
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
c.tp.WritePacket(pkt)
|
||||
return nil
|
||||
}
|
||||
func (c *Core) AddDevice(msg *sip.Message) *Device {
|
||||
v := &Device{
|
||||
ID: msg.From.Uri.UserInfo(),
|
||||
RegisterTime: time.Now(),
|
||||
UpdateTime: time.Now(),
|
||||
Status: string(sip.REGISTER),
|
||||
core: c,
|
||||
from: &sip.Contact{Uri: msg.StartLine.Uri, Params: make(map[string]string)},
|
||||
to: msg.To,
|
||||
host: msg.Via.Host,
|
||||
port: msg.Via.Port,
|
||||
}
|
||||
c.Devices.Store(msg.From.Uri.UserInfo(), v)
|
||||
return v
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"github.com/Monibuca/plugin-gb28181/utils"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Channel struct {
|
||||
DeviceID string
|
||||
Name string
|
||||
Manufacturer string
|
||||
Model string
|
||||
Owner string
|
||||
CivilCode string
|
||||
Address string
|
||||
Parental int
|
||||
SafetyWay int
|
||||
RegisterWay int
|
||||
Secrecy int
|
||||
Status string
|
||||
device *Device
|
||||
inviteRes *sip.Message
|
||||
Connected bool
|
||||
}
|
||||
type Device struct {
|
||||
ID string
|
||||
RegisterTime time.Time
|
||||
UpdateTime time.Time
|
||||
Status string
|
||||
Channels []Channel
|
||||
core *Core
|
||||
sn int
|
||||
from *sip.Contact
|
||||
to *sip.Contact
|
||||
host string
|
||||
port string
|
||||
}
|
||||
|
||||
func (d *Device) UpdateChannels(list []Channel) {
|
||||
for _, c := range list {
|
||||
c.device = d
|
||||
have := false
|
||||
for i, o := range d.Channels {
|
||||
if o.DeviceID == c.DeviceID {
|
||||
c.inviteRes = o.inviteRes
|
||||
c.Connected = o.inviteRes!=nil
|
||||
d.Channels[i] = c
|
||||
have = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !have {
|
||||
d.Channels = append(d.Channels, c)
|
||||
}
|
||||
}
|
||||
}
|
||||
func (c *Channel) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
|
||||
requestMsg = c.device.CreateMessage(Method)
|
||||
requestMsg.StartLine.Uri = sip.NewURI(c.DeviceID + "@" + c.device.to.Uri.Domain())
|
||||
requestMsg.To = &sip.Contact{
|
||||
Uri: requestMsg.StartLine.Uri,
|
||||
}
|
||||
requestMsg.From = &sip.Contact{
|
||||
Uri: sip.NewURI(c.device.core.config.Serial + "@" + c.device.core.config.Realm),
|
||||
Params: map[string]string{"tag": utils.RandNumString(9)},
|
||||
}
|
||||
return
|
||||
}
|
||||
func (d *Device) CreateMessage(Method sip.Method) (requestMsg *sip.Message) {
|
||||
d.sn++
|
||||
requestMsg = &sip.Message{
|
||||
Mode: sip.SIP_MESSAGE_REQUEST,
|
||||
MaxForwards: 70,
|
||||
UserAgent: "Monibuca",
|
||||
StartLine: &sip.StartLine{
|
||||
Method: Method,
|
||||
Uri: d.to.Uri,
|
||||
}, Via: &sip.Via{
|
||||
Transport: "UDP",
|
||||
Host: d.core.config.SipIP,
|
||||
Port: fmt.Sprintf("%d", d.core.config.SipPort),
|
||||
Params: map[string]string{
|
||||
"branch": fmt.Sprintf("z9hG4bK%s", utils.RandNumString(8)),
|
||||
"rport": "-1", //only key,no-value
|
||||
},
|
||||
}, From: d.from,
|
||||
To: d.to, CSeq: &sip.CSeq{
|
||||
ID: 1,
|
||||
Method: Method,
|
||||
}, CallID: utils.RandNumString(10),
|
||||
Addr: d.host + ":" + d.port,
|
||||
}
|
||||
requestMsg.From.Params["tag"] = utils.RandNumString(9)
|
||||
return
|
||||
}
|
||||
func (d *Device) Query() int {
|
||||
requestMsg := d.CreateMessage(sip.MESSAGE)
|
||||
requestMsg.ContentType = "Application/MANSCDP+xml"
|
||||
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
|
||||
<Query>
|
||||
<CmdType>Catalog</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
</Query>`, d.sn, requestMsg.To.Uri.UserInfo())
|
||||
requestMsg.ContentLength = len(requestMsg.Body)
|
||||
return d.core.SendMessage(requestMsg).Code
|
||||
}
|
||||
func (d *Device) Control(channelIndex int, PTZCmd string) int {
|
||||
channel := &d.Channels[channelIndex]
|
||||
requestMsg := channel.CreateMessage(sip.MESSAGE)
|
||||
requestMsg.ContentType = "Application/MANSCDP+xml"
|
||||
requestMsg.Body = fmt.Sprintf(`<?xml version="1.0"?>
|
||||
<Control>
|
||||
<CmdType>DeviceControl</CmdType>
|
||||
<SN>%d</SN>
|
||||
<DeviceID>%s</DeviceID>
|
||||
<PTZCmd>%s</PTZCmd>
|
||||
</Control>`, d.sn, requestMsg.To.Uri.UserInfo(), PTZCmd)
|
||||
requestMsg.ContentLength = len(requestMsg.Body)
|
||||
return d.core.SendMessage(requestMsg).Code
|
||||
}
|
||||
func (d *Device) Invite(channelIndex int) int {
|
||||
channel := &d.Channels[channelIndex]
|
||||
port := d.core.OnInvite(channel)
|
||||
if port == 0 {
|
||||
return 304
|
||||
}
|
||||
sdp := fmt.Sprintf(`v=0
|
||||
o=%s 0 0 IN IP4 %s
|
||||
s=Play
|
||||
c=IN IP4 %s
|
||||
t=0 0
|
||||
m=video %d RTP/AVP 96 98 97
|
||||
a=recvonly
|
||||
a=rtpmap:96 PS/90000
|
||||
a=rtpmap:97 MPEG4/90000
|
||||
a=rtpmap:98 H264/90000
|
||||
y=0200000001
|
||||
`, d.core.config.Serial, d.core.config.MediaIP, d.core.config.MediaIP, port)
|
||||
sdp = strings.ReplaceAll(sdp, "\n", "\r\n")
|
||||
invite := channel.CreateMessage(sip.INVITE)
|
||||
invite.ContentType = "application/sdp"
|
||||
invite.Contact = &sip.Contact{
|
||||
Uri: sip.NewURI(fmt.Sprintf("%s@%s:%d", d.core.config.Serial, d.core.config.SipIP, d.core.config.SipPort)),
|
||||
}
|
||||
invite.Body = sdp
|
||||
invite.ContentLength = len(sdp)
|
||||
invite.Subject = fmt.Sprintf("%s:0200000001,34020000002020000001:0", channel.DeviceID)
|
||||
response := d.core.SendMessage(invite)
|
||||
fmt.Printf("invite response statuscode: %d\n", response.Code)
|
||||
if response.Code == 200 {
|
||||
channel.inviteRes = response.Data
|
||||
channel.Connected = true
|
||||
channel.Ack()
|
||||
}
|
||||
return response.Code
|
||||
}
|
||||
func (d *Device) Bye(channelIndex int) int{
|
||||
channel := &d.Channels[channelIndex]
|
||||
defer func() {
|
||||
channel.inviteRes = nil
|
||||
channel.Connected = false
|
||||
}()
|
||||
return channel.Bye().Code
|
||||
}
|
||||
func (c *Channel) Ack() {
|
||||
ack := c.CreateMessage(sip.ACK)
|
||||
ack.From = c.inviteRes.From
|
||||
ack.To = c.inviteRes.To
|
||||
ack.CallID = c.inviteRes.CallID
|
||||
go c.device.core.Send(ack)
|
||||
}
|
||||
func (c *Channel) Bye() *Response{
|
||||
bye := c.CreateMessage(sip.BYE)
|
||||
bye.From = c.inviteRes.From
|
||||
bye.To = c.inviteRes.To
|
||||
bye.CallID = c.inviteRes.CallID
|
||||
return c.device.core.SendMessage(bye)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"errors"
|
||||
)
|
||||
|
||||
//transaction 的错误定义
|
||||
var (
|
||||
ErrorSyntax = errors.New("message syntax error")
|
||||
ErrorCheck = errors.New("message check failed")
|
||||
ErrorParse = errors.New("message parse failed")
|
||||
ErrorUnknown = errors.New("message unknown")
|
||||
)
|
||||
@@ -1,160 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
|INVITE from TU
|
||||
Timer A fires |INVITE sent
|
||||
Reset A, V Timer B fires
|
||||
INVITE sent +-----------+ or Transport Err.
|
||||
+---------| |---------------+inform TU
|
||||
| | Calling | |
|
||||
+-------->| |-------------->|
|
||||
+-----------+ 2xx |
|
||||
| | 2xx to TU |
|
||||
| |1xx |
|
||||
300-699 +---------------+ |1xx to TU |
|
||||
ACK sent | | |
|
||||
resp. to TU | 1xx V |
|
||||
| 1xx to TU -----------+ |
|
||||
| +---------| | |
|
||||
| | |Proceeding |-------------->|
|
||||
| +-------->| | 2xx |
|
||||
| +-----------+ 2xx to TU |
|
||||
| 300-699 | |
|
||||
| ACK sent, | |
|
||||
| resp. to TU| |
|
||||
| | | NOTE:
|
||||
| 300-699 V |
|
||||
| ACK sent +-----------+Transport Err. | transitions
|
||||
| +---------| |Inform TU | labeled with
|
||||
| | | Completed |-------------->| the event
|
||||
| +-------->| | | over the action
|
||||
| +-----------+ | to take
|
||||
| ^ | |
|
||||
| | | Timer D fires |
|
||||
+--------------+ | - |
|
||||
| |
|
||||
V |
|
||||
+-----------+ |
|
||||
| | |
|
||||
| Terminated|<--------------+
|
||||
| |
|
||||
+-----------+
|
||||
|
||||
Figure 5: INVITE client transaction
|
||||
*/
|
||||
func ict_snd_invite(t *Transaction, e *EventObj) error {
|
||||
msg := e.msg
|
||||
|
||||
t.isReliable = msg.IsReliable()
|
||||
t.origRequest = msg
|
||||
t.state = ICT_CALLING
|
||||
|
||||
//发送出去之后,开启 timer
|
||||
if msg.IsReliable() {
|
||||
//stop timer E in reliable transport
|
||||
fmt.Println("Reliabel")
|
||||
} else {
|
||||
fmt.Println("Not Reliable")
|
||||
//发送定时器,每次加倍,没有上限?
|
||||
t.timerA = NewSipTimer(T1, 0, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_A,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//事物定时器
|
||||
t.timerB = time.AfterFunc(TimeB, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_B,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func osip_ict_timeout_a_event(t *Transaction, e *EventObj) error {
|
||||
err := t.SipSend(t.origRequest)
|
||||
if err != nil {
|
||||
//发送失败
|
||||
t.Terminate()
|
||||
return err
|
||||
}
|
||||
t.timerA.Reset(t.timerA.timeout * 2)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func osip_ict_timeout_b_event(t *Transaction, e *EventObj) error {
|
||||
t.Terminate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func ict_rcv_1xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
t.state = ICT_PROCEEDING
|
||||
return nil
|
||||
}
|
||||
func ict_rcv_2xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
|
||||
t.Terminate()
|
||||
|
||||
return nil
|
||||
}
|
||||
func ict_rcv_3456xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
|
||||
if t.state != ICT_COMPLETED {
|
||||
/* not a retransmission */
|
||||
/* automatic handling of ack! */
|
||||
ack := ict_create_ack(t, e.msg)
|
||||
t.ack = ack
|
||||
_ = t.SipSend(t.ack)
|
||||
t.Terminate()
|
||||
}
|
||||
|
||||
/* start timer D (length is set to MAX (64*DEFAULT_T1 or 32000) */
|
||||
t.timerD = time.AfterFunc(TimeD, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_D,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
|
||||
t.state = ICT_COMPLETED
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ict_create_ack(t *Transaction, resp *sip.Message) *sip.Message {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ict_retransmit_ack(t *Transaction, e *EventObj) error {
|
||||
if t.ack == nil {
|
||||
/* ??? we should make a new ACK and send it!!! */
|
||||
return nil
|
||||
}
|
||||
|
||||
err := t.SipSend(t.ack)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.state = ICT_COMPLETED
|
||||
return nil
|
||||
}
|
||||
|
||||
func osip_ict_timeout_d_event(t *Transaction, e *EventObj) error {
|
||||
t.Terminate()
|
||||
return nil
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package transaction
|
||||
|
||||
func ist_rcv_invite(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func osip_ist_timeout_g_event(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func osip_ist_timeout_h_event(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func ist_snd_1xx(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func ist_snd_2xx(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func ist_snd_3456xx(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func ist_rcv_ack(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
func osip_ist_timeout_i_event(t *Transaction, e *EventObj) error {
|
||||
return nil
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
非invite事物的状态机:
|
||||
|
||||
|Request from TU
|
||||
|send request
|
||||
Timer E V
|
||||
send request +-----------+
|
||||
+---------| |-------------------+
|
||||
| | Trying | Timer F |
|
||||
+-------->| | or Transport Err.|
|
||||
+-----------+ inform TU |
|
||||
200-699 | | |
|
||||
resp. to TU | |1xx |
|
||||
+---------------+ |resp. to TU |
|
||||
| | |
|
||||
| Timer E V Timer F |
|
||||
| send req +-----------+ or Transport Err. |
|
||||
| +---------| | inform TU |
|
||||
| | |Proceeding |------------------>|
|
||||
| +-------->| |-----+ |
|
||||
| +-----------+ |1xx |
|
||||
| | ^ |resp to TU |
|
||||
| 200-699 | +--------+ |
|
||||
| resp. to TU | |
|
||||
| | |
|
||||
| V |
|
||||
| +-----------+ |
|
||||
| | | |
|
||||
| | Completed | |
|
||||
| | | |
|
||||
| +-----------+ |
|
||||
| ^ | |
|
||||
| | | Timer K |
|
||||
+--------------+ | - |
|
||||
| |
|
||||
V |
|
||||
NOTE: +-----------+ |
|
||||
| | |
|
||||
transitions | Terminated|<------------------+
|
||||
labeled with | |
|
||||
the event +-----------+
|
||||
over the action
|
||||
to take
|
||||
|
||||
Figure 6: non-INVITE client transaction
|
||||
*/
|
||||
func nict_snd_request(t *Transaction, e *EventObj) error {
|
||||
msg := e.msg
|
||||
fmt.Println("nict request:", msg.GetMethod())
|
||||
|
||||
t.origRequest = msg
|
||||
t.state = NICT_TRYING
|
||||
|
||||
err := t.SipSend(msg)
|
||||
if err != nil {
|
||||
t.Terminate()
|
||||
return err
|
||||
}
|
||||
|
||||
//发送出去之后,开启 timer
|
||||
if msg.IsReliable() {
|
||||
//stop timer E in reliable transport
|
||||
fmt.Println("Reliabel")
|
||||
} else {
|
||||
fmt.Println("Not Reliable")
|
||||
//发送定时器
|
||||
t.timerE = NewSipTimer(T1, T2, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_E,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
//事物定时器
|
||||
t.timerF = time.AfterFunc(TimeF, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_F,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
//事物超时
|
||||
func osip_nict_timeout_f_event(t *Transaction, e *EventObj) error {
|
||||
t.Terminate()
|
||||
return nil
|
||||
}
|
||||
|
||||
func osip_nict_timeout_e_event(t *Transaction, e *EventObj) error {
|
||||
if t.state == NICT_TRYING {
|
||||
//reset timer
|
||||
t.timerE.Reset(t.timerE.timeout * 2)
|
||||
} else {
|
||||
//in PROCEEDING STATE, TIMER is always T2
|
||||
t.timerE.Reset(T2)
|
||||
}
|
||||
|
||||
//resend origin request
|
||||
err := t.SipSend(t.origRequest)
|
||||
if err != nil {
|
||||
t.Terminate()
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func nict_rcv_1xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
t.state = NICT_PROCEEDING
|
||||
|
||||
//重置发送定时器
|
||||
t.timerE.Reset(T2)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func nict_rcv_23456xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
t.state = NICT_COMPLETED
|
||||
|
||||
if e.msg.IsReliable() {
|
||||
//不设置timerK
|
||||
} else {
|
||||
t.timerK = time.AfterFunc(T4, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_K,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func osip_nict_timeout_k_event(t *Transaction, e *EventObj) error {
|
||||
t.Terminate()
|
||||
return nil
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
|Request received
|
||||
|pass to TU
|
||||
V
|
||||
+-----------+
|
||||
| |
|
||||
| Trying |-------------+
|
||||
| | |
|
||||
+-----------+ |200-699 from TU
|
||||
| |send response
|
||||
|1xx from TU |
|
||||
|send response |
|
||||
| |
|
||||
Request V 1xx from TU |
|
||||
send response+-----------+send response|
|
||||
+--------| |--------+ |
|
||||
| | Proceeding| | |
|
||||
+------->| |<-------+ |
|
||||
+<--------------| | |
|
||||
|Trnsprt Err +-----------+ |
|
||||
|Inform TU | |
|
||||
| | |
|
||||
| |200-699 from TU |
|
||||
| |send response |
|
||||
| Request V |
|
||||
| send response+-----------+ |
|
||||
| +--------| | |
|
||||
| | | Completed |<------------+
|
||||
| +------->| |
|
||||
+<--------------| |
|
||||
|Trnsprt Err +-----------+
|
||||
|Inform TU |
|
||||
| |Timer J fires
|
||||
| |-
|
||||
| |
|
||||
| V
|
||||
| +-----------+
|
||||
| | |
|
||||
+-------------->| Terminated|
|
||||
| |
|
||||
+-----------+
|
||||
|
||||
Figure 8: non-INVITE server transaction
|
||||
|
||||
*/
|
||||
|
||||
func nist_rcv_request(t *Transaction, e *EventObj) error {
|
||||
fmt.Println("rcv request: ", e.msg.GetMethod())
|
||||
fmt.Println("transaction state: ", t.state.String())
|
||||
if t.state != NIST_PRE_TRYING {
|
||||
fmt.Println("rcv request retransmission,do response")
|
||||
if t.lastResponse != nil {
|
||||
err := t.SipSend(t.lastResponse)
|
||||
if err != nil {
|
||||
//transport error
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
t.origRequest = e.msg
|
||||
t.state = NIST_TRYING
|
||||
t.isReliable = e.msg.IsReliable()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func nist_snd_1xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
err := t.SipSend(t.lastResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.state = NIST_PROCEEDING
|
||||
return nil
|
||||
}
|
||||
|
||||
func nist_snd_23456xx(t *Transaction, e *EventObj) error {
|
||||
t.lastResponse = e.msg
|
||||
err := t.SipSend(t.lastResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if t.state != NIST_COMPLETED {
|
||||
if !t.isReliable {
|
||||
t.timerJ = time.AfterFunc(T1*64, func() {
|
||||
t.event <- &EventObj{
|
||||
evt: TIMEOUT_J,
|
||||
tid: t.id,
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
t.state = NIST_COMPLETED
|
||||
return nil
|
||||
}
|
||||
func osip_nist_timeout_j_event(t *Transaction, e *EventObj) error {
|
||||
t.Terminate()
|
||||
return nil
|
||||
}
|
||||
@@ -1,449 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"github.com/Monibuca/plugin-gb28181/transport"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
//状态机之状态
|
||||
type State int
|
||||
|
||||
const (
|
||||
/* STATES for invite client transaction */
|
||||
ICT_PRE_CALLING State = iota
|
||||
ICT_CALLING
|
||||
ICT_PROCEEDING
|
||||
ICT_COMPLETED
|
||||
ICT_TERMINATED
|
||||
|
||||
/* STATES for invite server transaction */
|
||||
IST_PRE_PROCEEDING
|
||||
IST_PROCEEDING
|
||||
IST_COMPLETED
|
||||
IST_CONFIRMED
|
||||
IST_TERMINATED
|
||||
|
||||
/* STATES for NON-invite client transaction */
|
||||
NICT_PRE_TRYING
|
||||
NICT_TRYING
|
||||
NICT_PROCEEDING
|
||||
NICT_COMPLETED
|
||||
NICT_TERMINATED
|
||||
|
||||
/* STATES for NON-invite server transaction */
|
||||
NIST_PRE_TRYING
|
||||
NIST_TRYING
|
||||
NIST_PROCEEDING
|
||||
NIST_COMPLETED
|
||||
NIST_TERMINATED
|
||||
|
||||
/* STATES for dialog */
|
||||
DIALOG_EARLY
|
||||
DIALOG_CONFIRMED
|
||||
DIALOG_CLOSE
|
||||
)
|
||||
|
||||
var stateMap = map[State]string{
|
||||
ICT_PRE_CALLING: "ICT_PRE_CALLING",
|
||||
ICT_CALLING: "ICT_CALLING",
|
||||
ICT_PROCEEDING: "ICT_PROCEEDING",
|
||||
ICT_COMPLETED: "ICT_COMPLETED",
|
||||
ICT_TERMINATED: "ICT_TERMINATED",
|
||||
IST_PRE_PROCEEDING: "IST_PRE_PROCEEDING",
|
||||
IST_PROCEEDING: "IST_PROCEEDING",
|
||||
IST_COMPLETED: "IST_COMPLETED",
|
||||
IST_CONFIRMED: "IST_CONFIRMED",
|
||||
IST_TERMINATED: "IST_TERMINATED",
|
||||
NICT_PRE_TRYING: "NICT_PRE_TRYING",
|
||||
NICT_TRYING: "NICT_TRYING",
|
||||
NICT_PROCEEDING: "NICT_PROCEEDING",
|
||||
NICT_COMPLETED: "NICT_COMPLETED",
|
||||
NICT_TERMINATED: "NICT_TERMINATED",
|
||||
NIST_PRE_TRYING: "NIST_PRE_TRYING",
|
||||
NIST_TRYING: "NIST_TRYING",
|
||||
NIST_PROCEEDING: "NIST_PROCEEDING",
|
||||
NIST_COMPLETED: "NIST_COMPLETED",
|
||||
NIST_TERMINATED: "NIST_TERMINATED",
|
||||
DIALOG_EARLY: "DIALOG_EARLY",
|
||||
DIALOG_CONFIRMED: "DIALOG_CONFIRMED",
|
||||
DIALOG_CLOSE: "DIALOG_CLOSE",
|
||||
}
|
||||
|
||||
func (s State) String() string {
|
||||
return stateMap[s]
|
||||
}
|
||||
|
||||
//状态机之事件
|
||||
type Event int
|
||||
|
||||
const (
|
||||
/* TIMEOUT EVENTS for ICT */
|
||||
TIMEOUT_A Event = iota /**< Timer A */
|
||||
TIMEOUT_B /**< Timer B */
|
||||
TIMEOUT_D /**< Timer D */
|
||||
|
||||
/* TIMEOUT EVENTS for NICT */
|
||||
TIMEOUT_E /**< Timer E */
|
||||
TIMEOUT_F /**< Timer F */
|
||||
TIMEOUT_K /**< Timer K */
|
||||
|
||||
/* TIMEOUT EVENTS for IST */
|
||||
TIMEOUT_G /**< Timer G */
|
||||
TIMEOUT_H /**< Timer H */
|
||||
TIMEOUT_I /**< Timer I */
|
||||
|
||||
/* TIMEOUT EVENTS for NIST */
|
||||
TIMEOUT_J /**< Timer J */
|
||||
|
||||
/* FOR INCOMING MESSAGE */
|
||||
RCV_REQINVITE /**< Event is an incoming INVITE request */
|
||||
RCV_REQACK /**< Event is an incoming ACK request */
|
||||
RCV_REQUEST /**< Event is an incoming NON-INVITE and NON-ACK request */
|
||||
RCV_STATUS_1XX /**< Event is an incoming informational response */
|
||||
RCV_STATUS_2XX /**< Event is an incoming 2XX response */
|
||||
RCV_STATUS_3456XX /**< Event is an incoming final response (not 2XX) */
|
||||
|
||||
/* FOR OUTGOING MESSAGE */
|
||||
SND_REQINVITE /**< Event is an outgoing INVITE request */
|
||||
SND_REQACK /**< Event is an outgoing ACK request */
|
||||
SND_REQUEST /**< Event is an outgoing NON-INVITE and NON-ACK request */
|
||||
SND_STATUS_1XX /**< Event is an outgoing informational response */
|
||||
SND_STATUS_2XX /**< Event is an outgoing 2XX response */
|
||||
SND_STATUS_3456XX /**< Event is an outgoing final response (not 2XX) */
|
||||
|
||||
KILL_TRANSACTION /**< Event to 'kill' the transaction before termination */
|
||||
UNKNOWN_EVT /**< Max event */
|
||||
)
|
||||
|
||||
var eventMap = map[Event]string{
|
||||
TIMEOUT_A: "TIMEOUT_A",
|
||||
TIMEOUT_B: "TIMEOUT_B",
|
||||
TIMEOUT_D: "TIMEOUT_D",
|
||||
TIMEOUT_E: "TIMEOUT_E",
|
||||
TIMEOUT_F: "TIMEOUT_F",
|
||||
TIMEOUT_K: "TIMEOUT_K",
|
||||
TIMEOUT_G: "TIMEOUT_G",
|
||||
TIMEOUT_H: "TIMEOUT_H",
|
||||
TIMEOUT_I: "TIMEOUT_I",
|
||||
TIMEOUT_J: "TIMEOUT_J",
|
||||
RCV_REQINVITE: "RCV_REQINVITE",
|
||||
RCV_REQACK: "RCV_REQACK",
|
||||
RCV_REQUEST: "RCV_REQUEST",
|
||||
RCV_STATUS_1XX: "RCV_STATUS_1XX",
|
||||
RCV_STATUS_2XX: "RCV_STATUS_2XX",
|
||||
RCV_STATUS_3456XX: "RCV_STATUS_3456XX",
|
||||
SND_REQINVITE: "SND_REQINVITE",
|
||||
SND_REQACK: "SND_REQACK",
|
||||
SND_REQUEST: "SND_REQUEST",
|
||||
SND_STATUS_1XX: "SND_STATUS_1XX",
|
||||
SND_STATUS_2XX: "SND_STATUS_2XX",
|
||||
SND_STATUS_3456XX: "SND_STATUS_3456XX",
|
||||
KILL_TRANSACTION: "KILL_TRANSACTION",
|
||||
UNKNOWN_EVT: "UNKNOWN_EVT",
|
||||
}
|
||||
|
||||
func (e Event) String() string {
|
||||
return eventMap[e]
|
||||
}
|
||||
|
||||
//incoming SIP MESSAGE
|
||||
func (e Event) IsIncomingMessage() bool {
|
||||
return e >= RCV_REQINVITE && e <= RCV_STATUS_3456XX
|
||||
}
|
||||
|
||||
//incoming SIP REQUEST
|
||||
func (e Event) IsIncomingRequest() bool {
|
||||
return e == RCV_REQINVITE || e == RCV_REQACK || e == RCV_REQUEST
|
||||
}
|
||||
|
||||
//incoming SIP RESPONSE
|
||||
func (e Event) IsIncomingResponse() bool {
|
||||
return e == RCV_STATUS_1XX || e == RCV_STATUS_2XX || e == RCV_STATUS_3456XX
|
||||
}
|
||||
|
||||
//outgoing SIP MESSAGE
|
||||
func (e Event) IsOutgoingMessage() bool {
|
||||
return e >= SND_REQINVITE && e <= SND_REQINVITE
|
||||
}
|
||||
|
||||
//outgoing SIP REQUEST
|
||||
func (e Event) IsOutgoingRequest() bool {
|
||||
return e == SND_REQINVITE || e == SND_REQACK || e == SND_REQUEST
|
||||
}
|
||||
|
||||
//outgoing SIP RESPONSE
|
||||
func (e Event) IsOutgoingResponse() bool {
|
||||
return e == SND_STATUS_1XX || e == SND_STATUS_2XX || e == SND_STATUS_3456XX
|
||||
}
|
||||
|
||||
//a SIP MESSAGE
|
||||
func (e Event) IsSipMessage() bool {
|
||||
return e >= RCV_REQINVITE && e <= SND_STATUS_3456XX
|
||||
}
|
||||
|
||||
type EventObj struct {
|
||||
evt Event // event type
|
||||
tid string // transaction id
|
||||
msg *sip.Message
|
||||
}
|
||||
|
||||
//状态机类型
|
||||
type FSMType int
|
||||
|
||||
const (
|
||||
FSM_ICT FSMType = iota /**< Invite Client (outgoing) Transaction */
|
||||
FSM_IST /**< Invite Server (incoming) Transaction */
|
||||
FSM_NICT /**< Non-Invite Client (outgoing) Transaction */
|
||||
FSM_NIST /**< Non-Invite Server (incoming) Transaction */
|
||||
FSM_UNKNOWN /**< Invalid Transaction */
|
||||
)
|
||||
|
||||
var typeMap = map[FSMType]string{
|
||||
FSM_ICT: "FSM_ICT",
|
||||
FSM_IST: "FSM_IST",
|
||||
FSM_NICT: "FSM_NICT",
|
||||
FSM_NIST: "FSM_NIST",
|
||||
FSM_UNKNOWN: "FSM_UNKNOWN",
|
||||
}
|
||||
|
||||
func (t FSMType) String() string {
|
||||
return typeMap[t]
|
||||
}
|
||||
|
||||
//对外将sip通讯封装成请求和响应
|
||||
//TODO:可参考http的request和response,屏蔽sip协议细节
|
||||
type Request struct {
|
||||
data *sip.Message
|
||||
}
|
||||
|
||||
//Code = 0,则响应正常
|
||||
//Code != 0,打印错误提示信息 Message
|
||||
type Response struct {
|
||||
Code int
|
||||
Message string
|
||||
Data *sip.Message
|
||||
}
|
||||
|
||||
type Handler func(t *Transaction, e *EventObj) error //操作
|
||||
|
||||
type Header map[string]string
|
||||
|
||||
// timer相关基础常量、方法等定义
|
||||
const (
|
||||
T1 = 100 * time.Millisecond
|
||||
T2 = 4 * time.Second
|
||||
T4 = 5 * time.Second
|
||||
TimeA = T1
|
||||
TimeB = 64 * T1
|
||||
TimeD = 32 * time.Second
|
||||
TimeE = T1
|
||||
TimeF = 64 * T1
|
||||
TimeG = T1
|
||||
TimeH = 64 * T1
|
||||
TimeI = T4
|
||||
TimeJ = 64 * T1
|
||||
TimeK = T4
|
||||
Time1xx = 100 * time.Millisecond
|
||||
)
|
||||
|
||||
//TODO:是否要管理当前 transaction 的多次请求和响应的message?
|
||||
//TODO:是否要管理当前 transaction 的头域
|
||||
//TODO:多种transaction在一个struct里面管理不太方便,暂时写在一起,后期重构分开,并使用interface 解耦
|
||||
|
||||
//是否需要tp layer?
|
||||
type Transaction struct {
|
||||
ctx context.Context //线程管理、其他参数
|
||||
id string //transaction ID
|
||||
isReliable bool //是否可靠传输
|
||||
core *Core //全局参数
|
||||
typo FSMType //状态机类型
|
||||
done chan struct{} //主动退出
|
||||
|
||||
state State //当前状态
|
||||
event chan *EventObj //输入的事件,带缓冲
|
||||
response chan *Response //输出的响应
|
||||
startAt time.Time //开始时间
|
||||
endAt time.Time //结束时间
|
||||
|
||||
//messages []*sip.Message //传输的消息缓存,origin request/last response/request ack...
|
||||
//header Header //创建事物的消息头域参数:Via From To CallID CSeq
|
||||
via *sip.Via
|
||||
from *sip.Contact
|
||||
to *sip.Contact
|
||||
callID string
|
||||
cseq *sip.CSeq
|
||||
origRequest *sip.Message //Initial request
|
||||
lastResponse *sip.Message //Last response,可能是临时的,也可能是最终的
|
||||
ack *sip.Message //ack request sent
|
||||
|
||||
//timer for ict
|
||||
timerA *SipTimer
|
||||
timerB *time.Timer
|
||||
timerD *time.Timer
|
||||
|
||||
//timer for nict
|
||||
timerE *SipTimer
|
||||
timerF *time.Timer
|
||||
timerK *time.Timer
|
||||
|
||||
//timer for ist
|
||||
timerG *time.Timer
|
||||
timerH *time.Timer
|
||||
timerI *time.Timer
|
||||
|
||||
//timer for nist
|
||||
timerJ *time.Timer
|
||||
}
|
||||
|
||||
type SipTimer struct {
|
||||
tm *time.Timer
|
||||
timeout time.Duration //当前超时时间
|
||||
max time.Duration //最大超时时间
|
||||
}
|
||||
|
||||
func NewSipTimer(d, max time.Duration, f func()) *SipTimer {
|
||||
return &SipTimer{
|
||||
tm: time.AfterFunc(d, f),
|
||||
timeout: d,
|
||||
max: max,
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SipTimer) Reset(d time.Duration) {
|
||||
t.timeout = d
|
||||
if t.timeout > t.max && t.max != 0 {
|
||||
t.timeout = t.max
|
||||
}
|
||||
t.tm.Reset(t.timeout)
|
||||
}
|
||||
|
||||
func (ta *Transaction) SetState(s State) {
|
||||
ta.state = s
|
||||
}
|
||||
|
||||
func (ta *Transaction) GetTid() string {
|
||||
return ta.id
|
||||
}
|
||||
|
||||
//每一个transaction至少有一个状态机线程运行
|
||||
//TODO:如果是一个uac的transaction,则把最后响应的消息返回(通过response chan)
|
||||
//transaction有很多消息需要传递到TU,也接收来自TU的消息。
|
||||
func (ta *Transaction) Run() {
|
||||
for {
|
||||
select {
|
||||
case e := <-ta.event:
|
||||
//根据event调用对应的handler
|
||||
fmt.Println("fsm run event:", e.evt.String())
|
||||
core := ta.core
|
||||
state := ta.state
|
||||
evtHandlers, ok1 := core.handlers[state]
|
||||
if !ok1 {
|
||||
fmt.Println("invalid state:", ta.state.String())
|
||||
break
|
||||
}
|
||||
f, ok2 := evtHandlers[e.evt]
|
||||
if !ok2 {
|
||||
fmt.Println("invalid handler for this event:", e.evt.String())
|
||||
break
|
||||
}
|
||||
fmt.Printf("state:%s, event:%s\n", state.String(), e.evt.String())
|
||||
err := f(ta, e)
|
||||
if err != nil {
|
||||
fmt.Printf("transaction run failed, state:%s, event:%s\n", state.String(), e.evt.String())
|
||||
}
|
||||
|
||||
case <-ta.done:
|
||||
fmt.Println("fsm exit")
|
||||
return
|
||||
|
||||
case <-ta.ctx.Done():
|
||||
fmt.Println("fsm killed")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//Terminated:事物的终止
|
||||
//TODO:check调用时机
|
||||
func (ta *Transaction) Terminate() {
|
||||
|
||||
ta.state = NICT_TERMINATED
|
||||
|
||||
switch ta.typo {
|
||||
case FSM_ICT:
|
||||
ta.state = ICT_TERMINATED
|
||||
case FSM_NICT:
|
||||
ta.state = NICT_TERMINATED
|
||||
case FSM_IST:
|
||||
ta.state = IST_TERMINATED
|
||||
case FSM_NIST:
|
||||
ta.state = NIST_TERMINATED
|
||||
}
|
||||
|
||||
//关掉事物的线程
|
||||
close(ta.done)
|
||||
|
||||
//TODO:某些timer需要检查并关掉,并且设置为nil
|
||||
|
||||
//remove ta from core
|
||||
ta.core.removeTa <- ta.id
|
||||
}
|
||||
|
||||
//根据sip消息,解析出目标服务器地址,发送消息
|
||||
func (ta *Transaction) SipSend(msg *sip.Message) error {
|
||||
err := checkMessage(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
addr := msg.Addr
|
||||
if addr==""{
|
||||
viaParams := msg.Via.Params
|
||||
//host
|
||||
var host, port string
|
||||
var ok1, ok2 bool
|
||||
|
||||
if host, ok1 = viaParams["maddr"]; !ok1 {
|
||||
if host, ok2 = viaParams["received"]; !ok2 {
|
||||
host = msg.Via.Host
|
||||
}
|
||||
}
|
||||
//port
|
||||
port = viaParams["rport"]
|
||||
if port == "" || port == "0" || port == "-1" {
|
||||
port = msg.Via.Port
|
||||
}
|
||||
|
||||
if port == "" {
|
||||
port = "5060"
|
||||
}
|
||||
|
||||
addr = fmt.Sprintf("%s:%s", host, port)
|
||||
}
|
||||
|
||||
fmt.Println("dest addr:", addr)
|
||||
|
||||
var err1, err2 error
|
||||
pkt := &transport.Packet{}
|
||||
pkt.Data, err1 = sip.Encode(msg)
|
||||
|
||||
if msg.Via.Transport == "UDP" {
|
||||
pkt.Addr, err2 = net.ResolveUDPAddr("udp", addr)
|
||||
} else {
|
||||
pkt.Addr, err2 = net.ResolveTCPAddr("tcp", addr)
|
||||
}
|
||||
|
||||
if err1 != nil {
|
||||
return err1
|
||||
}
|
||||
|
||||
if err2 != nil {
|
||||
return err2
|
||||
}
|
||||
ta.core.tp.WritePacket(pkt)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package transaction
|
||||
|
||||
import (
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//=====================================================sip message utils
|
||||
//The branch ID parameter in the Via header field values serves as a transaction identifier,
|
||||
//and is used by proxies to detect loops.
|
||||
//The branch parameter in the topmost Via header field of the request
|
||||
// is examined. If it is present and begins with the magic cookie
|
||||
// "z9hG4bK", the request was generated by a client transaction
|
||||
// compliant to this specification.
|
||||
//参考RFC3261
|
||||
func getMessageTransactionID(m *sip.Message) string {
|
||||
if m.GetMethod() == sip.ACK {
|
||||
//TODO:在匹配服务端事物的ACK中,创建事务的请求的方法为INVITE。所以ACK消息匹配事物的时候需要注意????
|
||||
}
|
||||
return string(m.GetMethod()) + "_" + m.GetBranch()
|
||||
}
|
||||
|
||||
//根据收到的响应的消息的状态码,获取事件
|
||||
func getInComingMessageEvent(m *sip.Message) Event {
|
||||
//request:根据请求方法来确认事件
|
||||
if m.IsRequest() {
|
||||
method := m.GetMethod()
|
||||
if method == sip.INVITE {
|
||||
return RCV_REQINVITE
|
||||
} else if method == sip.ACK {
|
||||
return RCV_REQACK
|
||||
} else {
|
||||
return RCV_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
//response:根据状态码来确认事件
|
||||
status := m.StartLine.Code
|
||||
if status >= 100 && status < 200 {
|
||||
return RCV_STATUS_1XX
|
||||
}
|
||||
|
||||
if status >= 200 && status < 300 {
|
||||
return RCV_STATUS_2XX
|
||||
}
|
||||
if status >= 300 {
|
||||
return RCV_STATUS_3456XX
|
||||
}
|
||||
|
||||
return UNKNOWN_EVT
|
||||
}
|
||||
|
||||
//根据发出的响应的消息的状态码,获取事件
|
||||
func getOutGoingMessageEvent(m *sip.Message) Event {
|
||||
//request:get event by method
|
||||
if m.IsRequest() {
|
||||
method := m.GetMethod()
|
||||
if method == sip.INVITE {
|
||||
return SND_REQINVITE
|
||||
} else if method == sip.ACK {
|
||||
return SND_REQACK
|
||||
} else {
|
||||
return SND_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
//response:get event by status
|
||||
status := m.StartLine.Code
|
||||
if status >= 100 && status < 200 {
|
||||
return SND_STATUS_1XX
|
||||
}
|
||||
|
||||
if status >= 200 && status < 300 {
|
||||
return SND_STATUS_2XX
|
||||
}
|
||||
if status >= 300 {
|
||||
return SND_STATUS_3456XX
|
||||
}
|
||||
|
||||
return UNKNOWN_EVT
|
||||
}
|
||||
|
||||
func checkMessage(msg *sip.Message) error {
|
||||
//TODO:sip消息解析成功之后,检查必要元素,如果失败,则返回 ErrorCheckMessage
|
||||
|
||||
//检查头域字段:callID via startline 等
|
||||
//检查seq、method等
|
||||
//不可以有router?
|
||||
//是否根据消息是接收还是发送检查?
|
||||
if msg == nil {
|
||||
return ErrorCheck
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//fix via header,add send-by info,
|
||||
func fixReceiveMessageViaParams(msg *sip.Message, addr net.Addr) {
|
||||
rport := msg.Via.Params["rport"]
|
||||
if rport == "" || rport == "0" || rport == "-1" {
|
||||
arr := strings.Split(addr.String(), ":")
|
||||
if len(arr) == 2 {
|
||||
msg.Via.Params["rport"] = arr[1]
|
||||
if msg.Via.Host != arr[0] {
|
||||
msg.Via.Params["received"] = arr[0]
|
||||
}
|
||||
} else {
|
||||
//TODO:数据报的地址错误??
|
||||
fmt.Println("packet handle > invalid addr :", addr.String())
|
||||
}
|
||||
} else {
|
||||
fmt.Println("sip message has have send-by info:", msg.Via.GetSendBy())
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
|
||||
#### 介绍
|
||||
|
||||
transport 包括客户端和服务器端,仅实现tcp和udp的传输层,不负责具体消息的解析和处理。不负责粘包、半包、消息解析、心跳处理、状态管理等工作。
|
||||
|
||||
比如设备关闭或者离线,要修改缓存状态、数据库状态、发送离线通知、执行离线回调等等,都在上层处理。tcp server 和 udp server、tcp client 和 udp client , 消息的接收和发送都在外面处理。
|
||||
|
||||
tcp是流传输,需要注意粘包和半包的处理。在上层处理tcp包的时候,可以尝试使用 ring buffer
|
||||
|
||||
|
||||
#### usage
|
||||
|
||||
参考 example.go
|
||||
|
||||
#### TODO
|
||||
|
||||
- sip协议的传输层,TCP和UDP有所不同,比如重传以及超时的错误信息等。所以需要在 transaction上面,处理消息重传、错误上报等。具体参考RFC3261。
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Monibuca/plugin-gb28181/utils"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
//默认端口:TCP/UDP是5060,5061是在TCP上的TLS
|
||||
//对于服务器监听UDP的任何端口和界面,都必须在TCP上也进行同样的监听。这是因为可能消息还需要通过TCP进行传输,比如消息过大的情况。
|
||||
const SipHost string = "127.0.0.1"
|
||||
const SipPort uint16 = 5060
|
||||
|
||||
func RunServerTCP() {
|
||||
tcp := NewTCPServer(SipPort, true)
|
||||
go PacketHandler(tcp)
|
||||
go func() {
|
||||
_ = tcp.Start()
|
||||
}()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
//测试通讯,客户端先发一条消息
|
||||
func RunClientTCP() {
|
||||
c := NewTCPClient(SipHost, SipPort)
|
||||
go PacketHandler(c)
|
||||
go func() {
|
||||
_ = c.Start()
|
||||
}()
|
||||
|
||||
//发送测试数据
|
||||
fmt.Println("send test data")
|
||||
go func() {
|
||||
for {
|
||||
c.WritePacket(&Packet{Data: []byte("from client : " + time.Now().String())})
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}()
|
||||
select {}
|
||||
}
|
||||
func PacketHandler(s ITransport) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("packet handler panic: ", err)
|
||||
utils.PrintStack()
|
||||
os.Exit(1)
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Println("PacketHandler ========== ", s.Name())
|
||||
|
||||
ch := s.ReadPacketChan()
|
||||
//阻塞读取消息
|
||||
for {
|
||||
select {
|
||||
case p := <-ch:
|
||||
fmt.Println("packet content:", string(p.Data))
|
||||
//TODO:message parse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//======================================================================
|
||||
|
||||
func RunServerUDP() {
|
||||
udp := NewUDPServer(SipPort)
|
||||
|
||||
go PacketHandler(udp)
|
||||
go func() {
|
||||
_ = udp.Start()
|
||||
}()
|
||||
|
||||
select {}
|
||||
}
|
||||
|
||||
func RunClientUDP() {
|
||||
c := NewUDPClient(SipHost, SipPort)
|
||||
go PacketHandler(c)
|
||||
go func() {
|
||||
_ = c.Start()
|
||||
}()
|
||||
//发送测试数据
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(1 * time.Second)
|
||||
c.WritePacket(&Packet{
|
||||
Data: []byte("hello " + time.Now().String())})
|
||||
}
|
||||
}()
|
||||
|
||||
select {}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
)
|
||||
|
||||
type TCPClient struct {
|
||||
Statistic
|
||||
host string
|
||||
port uint16
|
||||
conn net.Conn
|
||||
readChan chan *Packet
|
||||
writeChan chan *Packet
|
||||
remoteAddr net.Addr
|
||||
localAddr net.Addr
|
||||
done chan struct{}
|
||||
}
|
||||
|
||||
func NewTCPClient(host string, port uint16) IClient {
|
||||
return &TCPClient{
|
||||
host: host,
|
||||
port: port,
|
||||
readChan: make(chan *Packet, 10),
|
||||
writeChan: make(chan *Packet, 10),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TCPClient) IsReliable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *TCPClient) Name() string {
|
||||
return fmt.Sprintf("tcp client to:%s", fmt.Sprintf("%s:%d", c.host, c.port))
|
||||
}
|
||||
|
||||
func (c *TCPClient) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
func (c *TCPClient) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *TCPClient) Start() error {
|
||||
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", c.host, c.port))
|
||||
if err != nil {
|
||||
fmt.Println("dial tcp server failed :", err.Error())
|
||||
return err
|
||||
}else{
|
||||
fmt.Println("start tcp client")
|
||||
}
|
||||
|
||||
c.conn = conn
|
||||
c.remoteAddr = conn.RemoteAddr()
|
||||
c.localAddr = conn.LocalAddr()
|
||||
|
||||
//开启写线程
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case p := <-c.writeChan:
|
||||
_, err := c.conn.Write(p.Data)
|
||||
if err != nil {
|
||||
fmt.Println("client write failed:", err.Error())
|
||||
_ = c.Close()
|
||||
return
|
||||
}
|
||||
case <-c.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
fmt.Println("start tcp client")
|
||||
fmt.Printf("remote addr: %s, local addr: %s\n", conn.RemoteAddr().String(), conn.LocalAddr().String())
|
||||
|
||||
//读线程,阻塞
|
||||
for {
|
||||
buf := make([]byte, 2048)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Println("tcp client read error:", err.Error())
|
||||
return err
|
||||
}
|
||||
c.readChan <- &Packet{
|
||||
Addr: c.remoteAddr,
|
||||
Data: buf[:n],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *TCPClient) ReadPacketChan() <-chan *Packet {
|
||||
return c.readChan
|
||||
}
|
||||
|
||||
func (c *TCPClient) WritePacket(packet *Packet) {
|
||||
c.writeChan <- packet
|
||||
}
|
||||
|
||||
func (c *TCPClient) Close() error {
|
||||
close(c.done)
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
//外部定期调用此接口,实现心跳
|
||||
func (c *TCPClient) Heartbeat(p *Packet) {
|
||||
if p == nil {
|
||||
p = &Packet{
|
||||
Data: []byte("ping"),
|
||||
}
|
||||
}
|
||||
c.WritePacket(p)
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TCPServer struct {
|
||||
Statistic
|
||||
addr string
|
||||
listener net.Listener
|
||||
readChan chan *Packet
|
||||
writeChan chan *Packet
|
||||
done chan struct{}
|
||||
Keepalive bool
|
||||
sessions sync.Map //key 是 remote-addr , value:*Connection。
|
||||
}
|
||||
|
||||
func NewTCPServer(port uint16, keepalive bool) IServer {
|
||||
tcpAddr := fmt.Sprintf(":%d", port)
|
||||
|
||||
return &TCPServer{
|
||||
addr: tcpAddr,
|
||||
Keepalive: keepalive,
|
||||
readChan: make(chan *Packet, 10),
|
||||
writeChan: make(chan *Packet, 10),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) IsReliable() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *TCPServer) Name() string {
|
||||
return fmt.Sprintf("tcp server at:%s", s.addr)
|
||||
}
|
||||
func (s *TCPServer) IsKeepalive() bool {
|
||||
return s.Keepalive
|
||||
}
|
||||
|
||||
func (s *TCPServer) Start() error {
|
||||
//监听端口
|
||||
//开启tcp连接线程
|
||||
var err error
|
||||
s.listener, err = net.Listen("tcp", s.addr)
|
||||
//s.listener, err = tls.Listen("tcp", s.tcpAddr, tlsConfig)
|
||||
if err != nil {
|
||||
fmt.Println("TCP Listen failed:", err)
|
||||
return err
|
||||
}
|
||||
defer s.listener.Close()
|
||||
|
||||
fmt.Println("start tcp server at: ", s.addr)
|
||||
|
||||
//心跳线程
|
||||
if s.Keepalive {
|
||||
//TODO:start heartbeat thread
|
||||
}
|
||||
//写线程
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case p := <-s.writeChan:
|
||||
val, ok := s.sessions.Load(p.Addr.String())
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c := val.(*Connection)
|
||||
_, _ = c.Conn.Write(p.Data)
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
}()
|
||||
|
||||
//读线程
|
||||
for {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
var tempDelay time.Duration // how long to sleep on accept failure
|
||||
fmt.Println("accept err :", err.Error())
|
||||
// 重连。参考http server
|
||||
if ne, ok := err.(net.Error); ok && ne.Temporary() {
|
||||
if tempDelay == 0 {
|
||||
tempDelay = 5 * time.Millisecond
|
||||
} else {
|
||||
tempDelay *= 2
|
||||
}
|
||||
|
||||
if max := 1 * time.Second; tempDelay > max {
|
||||
tempDelay = max
|
||||
}
|
||||
|
||||
time.Sleep(tempDelay)
|
||||
continue
|
||||
}
|
||||
fmt.Println("accept error, retry failed & exit.")
|
||||
return err
|
||||
}
|
||||
|
||||
// conn.SetReadDeadline(time.Now().Add(600 * time.Second))
|
||||
session := &Connection{Conn: conn, Addr: conn.RemoteAddr()}
|
||||
address := session.Addr.String()
|
||||
s.sessions.Store(address, session)
|
||||
|
||||
fmt.Println(fmt.Sprintf("new tcp client remoteAddr: %v", address))
|
||||
go s.handlerSession(session)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) handlerSession(c *Connection) {
|
||||
addrStr := c.Addr.String()
|
||||
|
||||
//recovery from panic
|
||||
defer func() {
|
||||
s.CloseOne(addrStr)
|
||||
if err := recover(); err != nil {
|
||||
fmt.Println("client receiver handler panic: ", err)
|
||||
}
|
||||
}()
|
||||
|
||||
buf := make([]byte, 2048)
|
||||
for {
|
||||
n, err := c.Conn.Read(buf)
|
||||
switch {
|
||||
case err == nil:
|
||||
p := &Packet{
|
||||
Addr: c.Addr,
|
||||
Data: buf[:n],
|
||||
}
|
||||
s.readChan <- p
|
||||
case err == io.EOF:
|
||||
fmt.Println(fmt.Sprintf("io.EOF,client close --- remoteAddr: %v", c.Addr))
|
||||
return
|
||||
case err != nil:
|
||||
fmt.Println("client other err: ", err)
|
||||
fmt.Println(fmt.Sprintf("client other err --- remoteAddr: %v", addrStr))
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *TCPServer) CloseOne(addr string) {
|
||||
val, ok := s.sessions.Load(addr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
c := val.(*Connection)
|
||||
_ = c.Conn.Close()
|
||||
s.sessions.Delete(addr)
|
||||
}
|
||||
|
||||
func (s *TCPServer) ReadPacketChan() <-chan *Packet {
|
||||
return s.readChan
|
||||
}
|
||||
func (s *TCPServer) WritePacket(packet *Packet) {
|
||||
s.writeChan <- packet
|
||||
}
|
||||
|
||||
func (s *TCPServer) Close() error {
|
||||
//TODO:TCP服务退出之前,需要先close掉所有客户端的连接
|
||||
s.sessions.Range(func(key, value interface{}) bool {
|
||||
c := value.(*Connection)
|
||||
_ = c.Conn.Close()
|
||||
s.sessions.Delete(key)
|
||||
return true
|
||||
})
|
||||
return nil
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
/*
|
||||
transport层仅实现数据的读写,连接的关闭
|
||||
|
||||
TCP和UDP的区别
|
||||
|
||||
- TCP面向链接,在服务关闭的时候,要先close掉所有客户端连接。所以使用一个map简单做session管理,key 是 remote address。
|
||||
- udp不需要管理session。
|
||||
*/
|
||||
|
||||
//transport server and client interface
|
||||
//对于面向连接的服务,需要有两个关闭接口:Close and CloseOne
|
||||
//非面向连接的服务,不必实现
|
||||
//TODO:心跳管理,使用timewheel
|
||||
|
||||
type ITransport interface {
|
||||
Name() string
|
||||
ReadPacketChan() <-chan *Packet //读消息,消息处理器需在循环中阻塞读取
|
||||
WritePacket(packet *Packet) //写消息
|
||||
Start() error //开启连接,阻塞接收消息
|
||||
Close() error //关闭连接
|
||||
IsReliable() bool //是否可靠传输
|
||||
}
|
||||
|
||||
type IServer interface {
|
||||
ITransport
|
||||
CloseOne(addr string) //对于关闭某个客户端连接,比如没有鉴权的非法链接,心跳超时等
|
||||
IsKeepalive() bool //persistent connection or not
|
||||
}
|
||||
|
||||
//transport 需要实现的接口如下
|
||||
type IClient interface {
|
||||
ITransport
|
||||
LocalAddr() net.Addr //本地地址
|
||||
RemoteAddr() net.Addr //远程地址
|
||||
Heartbeat(packet *Packet) //客户端需要定期发送心跳包到服务器端
|
||||
}
|
||||
|
||||
type Packet struct {
|
||||
Type string //消息类型,预留字段,对于客户端主动关闭等消息的上报、心跳超时等。如果为空,则仅透传消息。
|
||||
Addr net.Addr
|
||||
Data []byte
|
||||
}
|
||||
|
||||
//对于面向连接的(UDP或者TCP都可以面向连接,维持心跳即可),必须有session
|
||||
type Connection struct {
|
||||
Addr net.Addr
|
||||
Conn net.Conn
|
||||
Online bool
|
||||
ReconnectCount int64 //重连次数
|
||||
}
|
||||
|
||||
func (s *Connection) Close() {
|
||||
//TODO:处理session的关闭,修改缓存状态、数据库状态、发送离线通知、执行离线回调等等
|
||||
}
|
||||
|
||||
//通讯统计
|
||||
type Statistic struct {
|
||||
startTime time.Time
|
||||
stopTime time.Time
|
||||
recvCount int64
|
||||
sendCount int64
|
||||
errCount int64
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
type UDPClient struct {
|
||||
Statistic
|
||||
host string
|
||||
port uint16
|
||||
conn *net.UDPConn
|
||||
readChan chan *Packet
|
||||
writeChan chan *Packet
|
||||
done chan struct{}
|
||||
remoteAddr net.Addr
|
||||
localAddr net.Addr
|
||||
}
|
||||
|
||||
func NewUDPClient(host string, port uint16) IClient {
|
||||
return &UDPClient{
|
||||
host: host,
|
||||
port: port,
|
||||
readChan: make(chan *Packet, 10),
|
||||
writeChan: make(chan *Packet, 10),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UDPClient) IsReliable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *UDPClient) Name() string {
|
||||
return fmt.Sprintf("udp client to:%s", fmt.Sprintf("%s:%d", c.host, c.port))
|
||||
}
|
||||
func (c *UDPClient) LocalAddr() net.Addr {
|
||||
return c.localAddr
|
||||
}
|
||||
|
||||
func (c *UDPClient) RemoteAddr() net.Addr {
|
||||
return c.remoteAddr
|
||||
}
|
||||
|
||||
func (c *UDPClient) Start() error {
|
||||
addrStr := fmt.Sprintf("%s:%d", c.host, c.port)
|
||||
addr, err := net.ResolveUDPAddr("udp", addrStr)
|
||||
if err != nil {
|
||||
fmt.Println("Can't resolve address: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
conn, err := net.DialUDP("udp", nil, addr)
|
||||
if err != nil {
|
||||
fmt.Println("Can't dial: ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
c.remoteAddr = conn.RemoteAddr()
|
||||
c.localAddr = conn.LocalAddr()
|
||||
|
||||
fmt.Println("udp client remote addr:", conn.RemoteAddr().String())
|
||||
fmt.Println("udp client local addr:", conn.LocalAddr().String())
|
||||
|
||||
//写线程
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case p := <-c.writeChan:
|
||||
_, err = conn.Write(p.Data)
|
||||
if err != nil {
|
||||
fmt.Println("udp client write failed:", err.Error())
|
||||
continue
|
||||
}
|
||||
case <-c.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
buf := make([]byte, 4096)
|
||||
n, err := conn.Read(buf)
|
||||
if err != nil {
|
||||
fmt.Println("failed to read UDP msg because of ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
c.readChan <- &Packet{
|
||||
Addr: c.remoteAddr,
|
||||
Data: buf[:n],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *UDPClient) ReadPacketChan() <-chan *Packet {
|
||||
return c.readChan
|
||||
}
|
||||
|
||||
func (c *UDPClient) WritePacket(packet *Packet) {
|
||||
c.writeChan <- packet
|
||||
}
|
||||
|
||||
func (c *UDPClient) Close() error {
|
||||
close(c.done)
|
||||
return c.conn.Close()
|
||||
}
|
||||
|
||||
//外部定期调用此接口,实现心跳
|
||||
func (c *UDPClient) Heartbeat(p *Packet) {
|
||||
if p == nil {
|
||||
p = &Packet{
|
||||
Data: []byte("ping"),
|
||||
}
|
||||
}
|
||||
c.WritePacket(p)
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
)
|
||||
|
||||
type UDPServer struct {
|
||||
Statistic
|
||||
addr string
|
||||
Conn *net.UDPConn
|
||||
ReadChan chan *Packet
|
||||
WriteChan chan *Packet
|
||||
done chan struct{}
|
||||
Keepalive bool
|
||||
//Sessions sync.Map // key is remote-addr的string , value:*Connection。UDP不需要
|
||||
}
|
||||
|
||||
func NewUDPServer(port uint16) IServer {
|
||||
addrStr := fmt.Sprintf(":%d", port)
|
||||
|
||||
return &UDPServer{
|
||||
addr: addrStr,
|
||||
ReadChan: make(chan *Packet, 10),
|
||||
WriteChan: make(chan *Packet, 10),
|
||||
done: make(chan struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UDPServer) IsReliable() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *UDPServer) Name() string {
|
||||
return fmt.Sprintf("udp client to:%s", s.addr)
|
||||
}
|
||||
|
||||
func (s *UDPServer) IsKeepalive() bool {
|
||||
return s.Keepalive
|
||||
}
|
||||
|
||||
func (s *UDPServer) Start() error {
|
||||
addr, err := net.ResolveUDPAddr("udp", s.addr)
|
||||
if err != nil {
|
||||
fmt.Println("Can't resolve address: ", err)
|
||||
return err
|
||||
}
|
||||
|
||||
conn, err := net.ListenUDP("udp", addr)
|
||||
if err != nil {
|
||||
fmt.Println("Error listenUDP :", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer func() {
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
s.Conn = conn
|
||||
|
||||
fmt.Println("start udp server at: ", s.addr)
|
||||
|
||||
//心跳线程
|
||||
if s.Keepalive {
|
||||
//TODO:start heartbeat thread
|
||||
}
|
||||
//写线程
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case p := <-s.WriteChan:
|
||||
_, _ = s.Conn.WriteTo(p.Data, p.Addr)
|
||||
case <-s.done:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
//读线程
|
||||
for {
|
||||
data := make([]byte, 4096)
|
||||
n, remoteAddr, err := conn.ReadFromUDP(data)
|
||||
if err != nil {
|
||||
fmt.Println("failed to read UDP msg because of ", err.Error())
|
||||
continue
|
||||
}
|
||||
s.ReadChan <- &Packet{
|
||||
Addr: remoteAddr,
|
||||
Data: data[:n],
|
||||
}
|
||||
}
|
||||
}
|
||||
func (s *UDPServer) ReadPacketChan() <-chan *Packet {
|
||||
return s.ReadChan
|
||||
}
|
||||
func (s *UDPServer) WritePacket(packet *Packet) {
|
||||
s.WriteChan <- packet
|
||||
}
|
||||
|
||||
func (s *UDPServer) Close() error {
|
||||
//所有session离线和关闭处理
|
||||
return nil
|
||||
}
|
||||
func (s *UDPServer) CloseOne(addr string) {
|
||||
//处理某设备离线
|
||||
}
|
||||
24
tu/README.md
24
tu/README.md
@@ -1,24 +0,0 @@
|
||||
|
||||
Transaction User(TU)事务用户:在transaction 层之上的协议层。TU包括了UAC core、UAS core,和proxy core。
|
||||
tu处理业务逻辑,并对事物层进行操作。
|
||||
|
||||
#### 类型
|
||||
|
||||
SIP服务器典型有以下几类:
|
||||
|
||||
a. 注册服务器 -即只管Register消息,这里相当于location也在这里了
|
||||
|
||||
b. 重定向服务器 -给ua回一条302后,转给其它的服务器,这样保证全系统统一接入
|
||||
|
||||
c. 代理服务器 -只做proxy,即对SIP消息转发
|
||||
|
||||
d. 媒体服务器-只做rtp包相关处理,即media server
|
||||
|
||||
e. B2BUA - 这个里包实际一般是可以含以上几种服务器类型
|
||||
|
||||
暂时仅处理gb28181 相关
|
||||
|
||||
#### TU
|
||||
|
||||
tu负责根据应用层需求,发起操作。
|
||||
比如注册到sip服务器、发起会话、取消会话等。
|
||||
93
tu/client.go
93
tu/client.go
@@ -1,93 +0,0 @@
|
||||
package tu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Monibuca/plugin-gb28181/transaction"
|
||||
)
|
||||
|
||||
//sip server和client的配置,可以得到sip URI:sip
|
||||
//格式:user:password@host:port;uri-parameters?headers
|
||||
//在这些URI里边包含了足够的信息来发起和维持到这个资源的一个通讯会话。
|
||||
//client静态配置
|
||||
type ClientStatic struct {
|
||||
LocalIP string //设备本地IP
|
||||
LocalPort uint16 //客户端SIP端口
|
||||
Username string //SIP用户名,一般是取通道ID,默认 34020000001320000001
|
||||
AuthID string //SIP用户认证ID,一般是通道ID, 默认 34020000001320000001
|
||||
Password string //密码
|
||||
}
|
||||
|
||||
//client运行时信息
|
||||
type ClientRuntime struct {
|
||||
RemoteAddress string //设备的公网的IP和端口,格式x.x.x.x:x
|
||||
Online bool //在线状态
|
||||
Branch string //branch
|
||||
Cseq int //消息序列号,发送消息递增, uint32
|
||||
FromTag string //from tag
|
||||
ToTag string //to tag
|
||||
Received string //remote ip
|
||||
Rport string //remote port
|
||||
}
|
||||
|
||||
type Client struct {
|
||||
*transaction.Core //transaction manager
|
||||
static *ClientStatic //静态配置
|
||||
runtime *ClientRuntime //运行时信息
|
||||
}
|
||||
|
||||
//config:sip信令服务器配置
|
||||
//static:sip客户端配置
|
||||
func NewClient(config *transaction.Config, static *ClientStatic) *Client {
|
||||
return &Client{
|
||||
Core: transaction.NewCore(config),
|
||||
static: static,
|
||||
runtime: &ClientRuntime{},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//TODO:对于一个TU,开启之后
|
||||
//运行一个sip client
|
||||
func RunClient() {
|
||||
config := &transaction.Config{
|
||||
SipIP: "192.168.1.102",
|
||||
SipPort: 5060,
|
||||
SipNetwork: "UDP",
|
||||
Serial: "34020000002000000001",
|
||||
Realm: "3402000000",
|
||||
AckTimeout: 10,
|
||||
|
||||
RegisterValidity: 3600,
|
||||
RegisterInterval: 60,
|
||||
HeartbeatInterval: 60,
|
||||
HeartbeatRetry: 3,
|
||||
|
||||
AudioEnable: true,
|
||||
WaitKeyFrame: true,
|
||||
MediaPortMin: 58200,
|
||||
MediaPortMax: 58300,
|
||||
MediaIdleTimeout: 30,
|
||||
}
|
||||
static := &ClientStatic{
|
||||
LocalIP: "192.168.1.65",
|
||||
LocalPort: 5060,
|
||||
Username: "34020000001320000001",
|
||||
AuthID: "34020000001320000001",
|
||||
Password: "123456",
|
||||
}
|
||||
c := NewClient(config, static)
|
||||
|
||||
go c.Start()
|
||||
|
||||
//TODO:先发起注册
|
||||
//TODO:build sip message
|
||||
msg := BuildMessageRequest("", "", "", "", "", "",
|
||||
0, 0, 0,"")
|
||||
resp := c.SendMessage(msg)
|
||||
if resp.Code != 0 {
|
||||
fmt.Println("request failed")
|
||||
}
|
||||
fmt.Println("response: ", resp.Data)
|
||||
|
||||
select {}
|
||||
}
|
||||
75
tu/common.go
75
tu/common.go
@@ -1,75 +0,0 @@
|
||||
package tu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/Monibuca/plugin-gb28181/sip"
|
||||
"github.com/Monibuca/plugin-gb28181/utils"
|
||||
)
|
||||
|
||||
//根据参数构建各种消息
|
||||
//参数来自于session/transaction等会话管理器
|
||||
/*
|
||||
method:请求方法
|
||||
transport:UDP/TCP
|
||||
sipSerial: sip server ID
|
||||
sipRealm: sip domain
|
||||
username: 用户名/设备序列号
|
||||
srcIP: 源IP
|
||||
srcPort:源端口
|
||||
expires: 过期时间
|
||||
cseq:消息序列号,当前对话递增
|
||||
*/
|
||||
//构建消息:以客户端(可能是IPC,也可能是SIP Server)的角度
|
||||
func BuildMessageRequest(method sip.Method, transport, sipSerial, sipRealm, username , srcIP string, srcPort uint16, expires, cseq int,body string) *sip.Message {
|
||||
server := fmt.Sprintf("%s@%s", sipSerial, sipRealm)
|
||||
client := fmt.Sprintf("%s@%s", username, sipRealm)
|
||||
|
||||
msg := &sip.Message{
|
||||
Mode: sip.SIP_MESSAGE_REQUEST,
|
||||
MaxForwards: 70,
|
||||
UserAgent: "IPC",
|
||||
Expires: expires,
|
||||
ContentLength: 0,
|
||||
}
|
||||
msg.StartLine = &sip.StartLine{
|
||||
Method: method,
|
||||
Uri: sip.NewURI(server),
|
||||
}
|
||||
msg.Via = &sip.Via{
|
||||
Transport: transport,
|
||||
Host: client,
|
||||
}
|
||||
msg.Via.Params = map[string]string{
|
||||
"branch": randBranch(),
|
||||
"rport": "-1", //only key,no-value
|
||||
}
|
||||
msg.From = &sip.Contact{
|
||||
Uri: sip.NewURI(client),
|
||||
Params: nil,
|
||||
}
|
||||
msg.From.Params = map[string]string{
|
||||
"tag": utils.RandNumString(10),
|
||||
}
|
||||
msg.To = &sip.Contact{
|
||||
Uri: sip.NewURI(client),
|
||||
}
|
||||
msg.CallID = utils.RandNumString(8)
|
||||
msg.CSeq = &sip.CSeq{
|
||||
ID: uint32(cseq),
|
||||
Method: method,
|
||||
}
|
||||
|
||||
msg.Contact = &sip.Contact{
|
||||
Uri: sip.NewURI(fmt.Sprintf("%s@%s:%d", username, srcIP, srcPort)),
|
||||
}
|
||||
if len(body)>0{
|
||||
msg.ContentLength = len(body)
|
||||
msg.Body = body
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
//z9hG4bK + 10个随机数字
|
||||
func randBranch() string {
|
||||
return fmt.Sprintf("z9hG4bK%s", utils.RandNumString(8))
|
||||
}
|
||||
48
tu/server.go
48
tu/server.go
@@ -1,48 +0,0 @@
|
||||
package tu
|
||||
|
||||
import (
|
||||
"github.com/Monibuca/plugin-gb28181/transaction"
|
||||
"sync"
|
||||
)
|
||||
|
||||
//TODO:参考http服务,使用者仅需要根据需要实现某些handler,替换某些header fileds or body信息。其他的处理都由库来实现。
|
||||
type Server struct {
|
||||
*transaction.Core //SIP transaction manager
|
||||
registers sync.Map //管理所有已经注册的设备端
|
||||
//routers:TODO:消息路由,应用层可以处理消息体,或者针对某些消息的callback
|
||||
}
|
||||
|
||||
//提供config参数
|
||||
func NewServer(config *transaction.Config) *Server {
|
||||
return &Server{
|
||||
Core: transaction.NewCore(config),
|
||||
}
|
||||
}
|
||||
|
||||
//运行一个sip server
|
||||
func RunServer() {
|
||||
config := &transaction.Config{
|
||||
SipIP: "192.168.1.102",
|
||||
SipPort: 5060,
|
||||
SipNetwork: "UDP",
|
||||
Serial: "34020000002000000001",
|
||||
Realm: "3402000000",
|
||||
AckTimeout: 10,
|
||||
|
||||
RegisterValidity: 3600,
|
||||
RegisterInterval: 60,
|
||||
HeartbeatInterval: 60,
|
||||
HeartbeatRetry: 3,
|
||||
|
||||
AudioEnable: true,
|
||||
WaitKeyFrame: true,
|
||||
MediaPortMin: 58200,
|
||||
MediaPortMax: 58300,
|
||||
MediaIdleTimeout: 30,
|
||||
}
|
||||
s := NewServer(config)
|
||||
|
||||
s.Start()
|
||||
|
||||
select {}
|
||||
}
|
||||
19
ui/dist/demo.html
vendored
19
ui/dist/demo.html
vendored
@@ -1,19 +0,0 @@
|
||||
<meta charset="utf-8">
|
||||
<title>plugin-gb28181 demo</title>
|
||||
<script src="https://unpkg.com/vue"></script>
|
||||
<script src="./plugin-gb28181.umd.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="./plugin-gb28181.css">
|
||||
|
||||
|
||||
<div id="app">
|
||||
<demo></demo>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
new Vue({
|
||||
components: {
|
||||
demo: plugin-gb28181
|
||||
}
|
||||
}).$mount('#app')
|
||||
</script>
|
||||
612
ui/dist/plugin-gb28181.common.js
vendored
612
ui/dist/plugin-gb28181.common.js
vendored
@@ -1,612 +0,0 @@
|
||||
module.exports =
|
||||
/******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = "deee");
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ({
|
||||
|
||||
/***/ "789d":
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
// extracted by mini-css-extract-plugin
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "b284":
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
/* harmony import */ var _plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("789d");
|
||||
/* harmony import */ var _plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__);
|
||||
/* unused harmony reexport * */
|
||||
/* unused harmony default export */ var _unused_webpack_default_export = (_plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0___default.a);
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "deee":
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js
|
||||
// This file is imported into lib/wc client bundles.
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
var currentScript = window.document.currentScript
|
||||
if (true) {
|
||||
var getCurrentScript = __webpack_require__("f3a8")
|
||||
currentScript = getCurrentScript()
|
||||
|
||||
// for backward compatibility, because previously we directly included the polyfill
|
||||
if (!('currentScript' in document)) {
|
||||
Object.defineProperty(document, 'currentScript', { get: getCurrentScript })
|
||||
}
|
||||
}
|
||||
|
||||
var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/)
|
||||
if (src) {
|
||||
__webpack_require__.p = src[1] // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate to webpack that this file can be concatenated
|
||||
/* harmony default export */ var setPublicPath = (null);
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"2507526a-vue-loader-template"}!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=template&id=764a9e60&
|
||||
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('mu-data-table',{attrs:{"data":_vm.Devices,"columns":_vm.columns},scopedSlots:_vm._u([{key:"expand",fn:function(prop){return [_c('mu-data-table',{attrs:{"data":prop.row.Channels,"columns":_vm.columns2},scopedSlots:_vm._u([{key:"default",fn:function(ref){
|
||||
var item = ref.row;
|
||||
var $index = ref.$index;
|
||||
return [_c('td',[_vm._v(_vm._s(item.DeviceID))]),_c('td',[_vm._v(_vm._s(item.Name))]),_c('td',[_vm._v(_vm._s(item.Manufacturer))]),_c('td',[_vm._v(_vm._s(item.Address))]),_c('td',[_vm._v(_vm._s(item.Status))]),_c('td',[(item.Connected)?_c('mu-button',{attrs:{"flat":""},on:{"click":function($event){return _vm.ptz(prop.row.ID, $index,item)}}},[_vm._v("云台")]):_vm._e(),(item.Connected)?_c('mu-button',{attrs:{"flat":""},on:{"click":function($event){return _vm.bye(prop.row.ID, $index)}}},[_vm._v("断开")]):_c('mu-button',{attrs:{"flat":""},on:{"click":function($event){return _vm.invite(prop.row.ID, $index,item)}}},[_vm._v("连接 ")])],1)]}}],null,true)})]}},{key:"default",fn:function(ref){
|
||||
var item = ref.row;
|
||||
return [_c('td',[_vm._v(_vm._s(item.ID))]),_c('td',[_vm._v(_vm._s(item.Channels ? item.Channels.length : 0))]),_c('td',[_c('StartTime',{attrs:{"value":item.RegisterTime}})],1),_c('td',[_c('StartTime',{attrs:{"value":item.UpdateTime}})],1),_c('td',[_vm._v(_vm._s(item.Status))])]}}])}),_c('webrtc-player',{ref:"player",attrs:{"PublicIP":_vm.PublicIP},on:{"ptz":_vm.sendPtz},model:{value:(_vm.previewStreamPath),callback:function ($$v) {_vm.previewStreamPath=$$v},expression:"previewStreamPath"}})],1)}
|
||||
var staticRenderFns = []
|
||||
|
||||
|
||||
// CONCATENATED MODULE: ./src/App.vue?vue&type=template&id=764a9e60&
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"2507526a-vue-loader-template"}!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/components/Player.vue?vue&type=template&id=70987c50&scoped=true&
|
||||
var Playervue_type_template_id_70987c50_scoped_true_render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('Modal',_vm._g(_vm._b({attrs:{"draggable":"","title":_vm.streamPath},on:{"on-ok":_vm.onClosePreview,"on-cancel":_vm.onClosePreview}},'Modal',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"container"},[_c('video',{ref:"webrtc",attrs:{"width":"488","height":"275","autoplay":"","muted":"","controls":""},domProps:{"srcObject":_vm.stream,"muted":true}}),_c('div',{staticClass:"control"},_vm._l((4),function(n){return _c('svg',{class:'arrow'+n,attrs:{"viewBox":"0 0 1024 1024","version":"1.1","xmlns":"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink","width":"64","height":"64"},on:{"click":function($event){return _vm.$emit('ptz',n)}}},[_c('defs'),_c('path',{attrs:{"d":"M682.666667 955.733333H341.333333a17.066667 17.066667 0 0 1-17.066666-17.066666V529.066667H85.333333a17.066667 17.066667 0 0 1-12.066133-29.1328l426.666667-426.666667a17.0496 17.0496 0 0 1 24.132266 0l426.666667 426.666667A17.066667 17.066667 0 0 1 938.666667 529.066667H699.733333v409.6a17.066667 17.066667 0 0 1-17.066666 17.066666z m-324.266667-34.133333h307.2V512a17.066667 17.066667 0 0 1 17.066667-17.066667h214.801066L512 109.4656 126.532267 494.933333H341.333333a17.066667 17.066667 0 0 1 17.066667 17.066667v409.6z","p-id":"6849"}})])}),0)]),_c('div',{attrs:{"slot":"footer"},slot:"footer"},[(_vm.remoteSDP)?_c('mu-badge',[_c('a',{attrs:{"slot":"content","href":_vm.remoteSDPURL,"download":"remoteSDP.txt"},slot:"content"},[_vm._v("remoteSDP")])]):_vm._e(),(_vm.localSDP)?_c('mu-badge',[_c('a',{attrs:{"slot":"content","href":_vm.localSDPURL,"download":"localSDP.txt"},slot:"content"},[_vm._v("localSDP")])]):_vm._e()],1)])}
|
||||
var Playervue_type_template_id_70987c50_scoped_true_staticRenderFns = []
|
||||
|
||||
|
||||
// CONCATENATED MODULE: ./src/components/Player.vue?vue&type=template&id=70987c50&scoped=true&
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/components/Player.vue?vue&type=script&lang=js&
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
let pc = null;
|
||||
/* harmony default export */ var Playervue_type_script_lang_js_ = ({
|
||||
data() {
|
||||
return {
|
||||
iceConnectionState: pc && pc.iceConnectionState,
|
||||
stream: null,
|
||||
localSDP: "",
|
||||
remoteSDP: "",
|
||||
remoteSDPURL: "",
|
||||
localSDPURL: "",
|
||||
streamPath: ""
|
||||
};
|
||||
},
|
||||
props:{
|
||||
PublicIP:String
|
||||
},
|
||||
methods: {
|
||||
async play(streamPath) {
|
||||
pc = new RTCPeerConnection();
|
||||
pc.addTransceiver('video',{
|
||||
direction:'recvonly'
|
||||
})
|
||||
this.streamPath = streamPath;
|
||||
pc.onsignalingstatechange = e => {
|
||||
//console.log(e);
|
||||
};
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
this.$toast.info(pc.iceConnectionState);
|
||||
this.iceConnectionState = pc.iceConnectionState;
|
||||
};
|
||||
pc.onicecandidate = event => {
|
||||
console.log(event)
|
||||
};
|
||||
pc.ontrack = event => {
|
||||
// console.log(event);
|
||||
if (event.track.kind == "video")
|
||||
this.stream = event.streams[0];
|
||||
};
|
||||
await pc.setLocalDescription(await pc.createOffer());
|
||||
this.localSDP = pc.localDescription.sdp;
|
||||
this.localSDPURL = URL.createObjectURL(
|
||||
new Blob([this.localSDP], { type: "text/plain" })
|
||||
);
|
||||
const result = await this.ajax({
|
||||
type: "POST",
|
||||
processData: false,
|
||||
data: JSON.stringify(pc.localDescription.toJSON()),
|
||||
url: "/webrtc/play?streamPath=" + this.streamPath,
|
||||
dataType: "json"
|
||||
});
|
||||
if (result.errmsg) {
|
||||
this.$toast.error(result.errmsg);
|
||||
return;
|
||||
} else {
|
||||
this.remoteSDP = result.sdp;
|
||||
this.remoteSDPURL = URL.createObjectURL(new Blob([this.remoteSDP], { type: "text/plain" }));
|
||||
}
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(result));
|
||||
},
|
||||
onClosePreview() {
|
||||
pc.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// CONCATENATED MODULE: ./src/components/Player.vue?vue&type=script&lang=js&
|
||||
/* harmony default export */ var components_Playervue_type_script_lang_js_ = (Playervue_type_script_lang_js_);
|
||||
// EXTERNAL MODULE: ./src/components/Player.vue?vue&type=style&index=0&id=70987c50&scoped=true&lang=css&
|
||||
var Playervue_type_style_index_0_id_70987c50_scoped_true_lang_css_ = __webpack_require__("b284");
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib/runtime/componentNormalizer.js
|
||||
/* globals __VUE_SSR_CONTEXT__ */
|
||||
|
||||
// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).
|
||||
// This module is a runtime utility for cleaner component module output and will
|
||||
// be included in the final webpack user bundle.
|
||||
|
||||
function normalizeComponent (
|
||||
scriptExports,
|
||||
render,
|
||||
staticRenderFns,
|
||||
functionalTemplate,
|
||||
injectStyles,
|
||||
scopeId,
|
||||
moduleIdentifier, /* server only */
|
||||
shadowMode /* vue-cli only */
|
||||
) {
|
||||
// Vue.extend constructor export interop
|
||||
var options = typeof scriptExports === 'function'
|
||||
? scriptExports.options
|
||||
: scriptExports
|
||||
|
||||
// render functions
|
||||
if (render) {
|
||||
options.render = render
|
||||
options.staticRenderFns = staticRenderFns
|
||||
options._compiled = true
|
||||
}
|
||||
|
||||
// functional template
|
||||
if (functionalTemplate) {
|
||||
options.functional = true
|
||||
}
|
||||
|
||||
// scopedId
|
||||
if (scopeId) {
|
||||
options._scopeId = 'data-v-' + scopeId
|
||||
}
|
||||
|
||||
var hook
|
||||
if (moduleIdentifier) { // server build
|
||||
hook = function (context) {
|
||||
// 2.3 injection
|
||||
context =
|
||||
context || // cached call
|
||||
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
||||
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
|
||||
// 2.2 with runInNewContext: true
|
||||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
||||
context = __VUE_SSR_CONTEXT__
|
||||
}
|
||||
// inject component styles
|
||||
if (injectStyles) {
|
||||
injectStyles.call(this, context)
|
||||
}
|
||||
// register component module identifier for async chunk inferrence
|
||||
if (context && context._registeredComponents) {
|
||||
context._registeredComponents.add(moduleIdentifier)
|
||||
}
|
||||
}
|
||||
// used by ssr in case component is cached and beforeCreate
|
||||
// never gets called
|
||||
options._ssrRegister = hook
|
||||
} else if (injectStyles) {
|
||||
hook = shadowMode
|
||||
? function () {
|
||||
injectStyles.call(
|
||||
this,
|
||||
(options.functional ? this.parent : this).$root.$options.shadowRoot
|
||||
)
|
||||
}
|
||||
: injectStyles
|
||||
}
|
||||
|
||||
if (hook) {
|
||||
if (options.functional) {
|
||||
// for template-only hot-reload because in that case the render fn doesn't
|
||||
// go through the normalizer
|
||||
options._injectStyles = hook
|
||||
// register for functional component in vue file
|
||||
var originalRender = options.render
|
||||
options.render = function renderWithStyleInjection (h, context) {
|
||||
hook.call(context)
|
||||
return originalRender(h, context)
|
||||
}
|
||||
} else {
|
||||
// inject component registration as beforeCreate hook
|
||||
var existing = options.beforeCreate
|
||||
options.beforeCreate = existing
|
||||
? [].concat(existing, hook)
|
||||
: [hook]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exports: scriptExports,
|
||||
options: options
|
||||
}
|
||||
}
|
||||
|
||||
// CONCATENATED MODULE: ./src/components/Player.vue
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* normalize component */
|
||||
|
||||
var component = normalizeComponent(
|
||||
components_Playervue_type_script_lang_js_,
|
||||
Playervue_type_template_id_70987c50_scoped_true_render,
|
||||
Playervue_type_template_id_70987c50_scoped_true_staticRenderFns,
|
||||
false,
|
||||
null,
|
||||
"70987c50",
|
||||
null
|
||||
|
||||
)
|
||||
|
||||
/* harmony default export */ var Player = (component.exports);
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=script&lang=js&
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
/* harmony default export */ var Appvue_type_script_lang_js_ = ({
|
||||
components:{
|
||||
WebrtcPlayer: Player
|
||||
},
|
||||
props:{
|
||||
ListenAddr:String
|
||||
},
|
||||
computed:{
|
||||
PublicIP(){
|
||||
return this.ListenAddr.split(":")[0]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Devices: [], previewStreamPath:false,
|
||||
context:{
|
||||
id:null,
|
||||
channel:0,
|
||||
item:null
|
||||
},
|
||||
columns: Object.freeze(
|
||||
["设备号", "通道数", "注册时间", "更新时间", "状态"].map(
|
||||
(title) => ({
|
||||
title,
|
||||
})
|
||||
)
|
||||
),
|
||||
columns2: Object.freeze([
|
||||
"通道编号",
|
||||
"名称",
|
||||
"厂商",
|
||||
"地址",
|
||||
"状态",
|
||||
"操作",
|
||||
]).map((title) => ({title})),
|
||||
ptzCmds:["A50F010800880045","A50F01018800003E", "A50F010400880041","A50F01028800003F"]
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchlist();
|
||||
},
|
||||
methods: {
|
||||
fetchlist() {
|
||||
const listES = new EventSource(this.apiHost + "/gb28181/list");
|
||||
listES.onmessage = (evt) => {
|
||||
if (!evt.data) return;
|
||||
this.Devices = JSON.parse(evt.data) || [];
|
||||
this.Devices.sort((a, b) => (a.ID > b.ID ? 1 : -1));
|
||||
};
|
||||
this.$once("hook:destroyed", () => listES.close());
|
||||
},
|
||||
ptz(id, channel,item) {
|
||||
this.context = {
|
||||
id,channel,item
|
||||
}
|
||||
this.previewStreamPath = true
|
||||
this.$nextTick(() =>this.$refs.player.play("gb28181/"+item.DeviceID));
|
||||
},
|
||||
sendPtz(n){
|
||||
this.ajax.get("/gb28181/control", {
|
||||
id:this.context.id,
|
||||
channel:this.context.channel,
|
||||
ptzcmd: this.ptzCmds[n-1],
|
||||
}).then(x=>{
|
||||
setTimeout(()=>{
|
||||
this.ajax.get("/gb28181/control", {
|
||||
id:this.context.id,
|
||||
channel:this.context.channel,
|
||||
ptzcmd: "A50F0100000000B5",
|
||||
});
|
||||
},500)
|
||||
});
|
||||
},
|
||||
invite(id, channel,item) {
|
||||
this.ajax.get("/gb28181/invite", {id, channel}).then(x=>{
|
||||
item.Connected = true
|
||||
});
|
||||
},
|
||||
bye(id, channel,item) {
|
||||
this.ajax.get("/gb28181/bye", {id, channel}).then(x=>{
|
||||
item.Connected = false
|
||||
});;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// CONCATENATED MODULE: ./src/App.vue?vue&type=script&lang=js&
|
||||
/* harmony default export */ var src_Appvue_type_script_lang_js_ = (Appvue_type_script_lang_js_);
|
||||
// CONCATENATED MODULE: ./src/App.vue
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* normalize component */
|
||||
|
||||
var App_component = normalizeComponent(
|
||||
src_Appvue_type_script_lang_js_,
|
||||
render,
|
||||
staticRenderFns,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
|
||||
)
|
||||
|
||||
/* harmony default export */ var App = (App_component.exports);
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/@vue/cli-service/lib/commands/build/entry-lib.js
|
||||
|
||||
|
||||
/* harmony default export */ var entry_lib = __webpack_exports__["default"] = (App);
|
||||
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "f3a8":
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// addapted from the document.currentScript polyfill by Adam Miller
|
||||
// MIT license
|
||||
// source: https://github.com/amiller-gh/currentScript-polyfill
|
||||
|
||||
// added support for Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1620505
|
||||
|
||||
(function (root, factory) {
|
||||
if (true) {
|
||||
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
||||
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
||||
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
|
||||
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
||||
} else {}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
function getCurrentScript () {
|
||||
if (document.currentScript) {
|
||||
return document.currentScript
|
||||
}
|
||||
|
||||
// IE 8-10 support script readyState
|
||||
// IE 11+ & Firefox support stack trace
|
||||
try {
|
||||
throw new Error();
|
||||
}
|
||||
catch (err) {
|
||||
// Find the second match for the "at" string to get file src url from stack.
|
||||
var ieStackRegExp = /.*at [^(]*\((.*):(.+):(.+)\)$/ig,
|
||||
ffStackRegExp = /@([^@]*):(\d+):(\d+)\s*$/ig,
|
||||
stackDetails = ieStackRegExp.exec(err.stack) || ffStackRegExp.exec(err.stack),
|
||||
scriptLocation = (stackDetails && stackDetails[1]) || false,
|
||||
line = (stackDetails && stackDetails[2]) || false,
|
||||
currentLocation = document.location.href.replace(document.location.hash, ''),
|
||||
pageSource,
|
||||
inlineScriptSourceRegExp,
|
||||
inlineScriptSource,
|
||||
scripts = document.getElementsByTagName('script'); // Live NodeList collection
|
||||
|
||||
if (scriptLocation === currentLocation) {
|
||||
pageSource = document.documentElement.outerHTML;
|
||||
inlineScriptSourceRegExp = new RegExp('(?:[^\\n]+?\\n){0,' + (line - 2) + '}[^<]*<script>([\\d\\D]*?)<\\/script>[\\d\\D]*', 'i');
|
||||
inlineScriptSource = pageSource.replace(inlineScriptSourceRegExp, '$1').trim();
|
||||
}
|
||||
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
// If ready state is interactive, return the script tag
|
||||
if (scripts[i].readyState === 'interactive') {
|
||||
return scripts[i];
|
||||
}
|
||||
|
||||
// If src matches, return the script tag
|
||||
if (scripts[i].src === scriptLocation) {
|
||||
return scripts[i];
|
||||
}
|
||||
|
||||
// If inline source matches, return the script tag
|
||||
if (
|
||||
scriptLocation === currentLocation &&
|
||||
scripts[i].innerHTML &&
|
||||
scripts[i].innerHTML.trim() === inlineScriptSource
|
||||
) {
|
||||
return scripts[i];
|
||||
}
|
||||
}
|
||||
|
||||
// If no match, return null
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return getCurrentScript
|
||||
}));
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ })["default"];
|
||||
//# sourceMappingURL=plugin-gb28181.common.js.map
|
||||
1
ui/dist/plugin-gb28181.common.js.map
vendored
1
ui/dist/plugin-gb28181.common.js.map
vendored
File diff suppressed because one or more lines are too long
1
ui/dist/plugin-gb28181.css
vendored
1
ui/dist/plugin-gb28181.css
vendored
@@ -1 +0,0 @@
|
||||
.arrow1[data-v-70987c50]{grid-column:2;grid-row:1}.arrow2[data-v-70987c50]{transform:rotate(90deg);grid-column:3;grid-row:2}.arrow3[data-v-70987c50]{transform:rotate(180deg);grid-column:2;grid-row:3}.arrow4[data-v-70987c50]{transform:rotate(270deg);grid-column:1;grid-row:2}.container[data-v-70987c50]{position:relative}.control[data-v-70987c50]{position:absolute;bottom:0;right:0;display:grid;grid-template-columns:repeat(3,33.33%);grid-template-rows:repeat(3,33.33%);width:192px;height:192px}.control>[data-v-70987c50]{cursor:pointer;fill:grey}.control>[data-v-70987c50]:hover{fill:#0ff}
|
||||
622
ui/dist/plugin-gb28181.umd.js
vendored
622
ui/dist/plugin-gb28181.umd.js
vendored
@@ -1,622 +0,0 @@
|
||||
(function webpackUniversalModuleDefinition(root, factory) {
|
||||
if(typeof exports === 'object' && typeof module === 'object')
|
||||
module.exports = factory();
|
||||
else if(typeof define === 'function' && define.amd)
|
||||
define([], factory);
|
||||
else if(typeof exports === 'object')
|
||||
exports["plugin-gb28181"] = factory();
|
||||
else
|
||||
root["plugin-gb28181"] = factory();
|
||||
})((typeof self !== 'undefined' ? self : this), function() {
|
||||
return /******/ (function(modules) { // webpackBootstrap
|
||||
/******/ // The module cache
|
||||
/******/ var installedModules = {};
|
||||
/******/
|
||||
/******/ // The require function
|
||||
/******/ function __webpack_require__(moduleId) {
|
||||
/******/
|
||||
/******/ // Check if module is in cache
|
||||
/******/ if(installedModules[moduleId]) {
|
||||
/******/ return installedModules[moduleId].exports;
|
||||
/******/ }
|
||||
/******/ // Create a new module (and put it into the cache)
|
||||
/******/ var module = installedModules[moduleId] = {
|
||||
/******/ i: moduleId,
|
||||
/******/ l: false,
|
||||
/******/ exports: {}
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Execute the module function
|
||||
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
|
||||
/******/
|
||||
/******/ // Flag the module as loaded
|
||||
/******/ module.l = true;
|
||||
/******/
|
||||
/******/ // Return the exports of the module
|
||||
/******/ return module.exports;
|
||||
/******/ }
|
||||
/******/
|
||||
/******/
|
||||
/******/ // expose the modules object (__webpack_modules__)
|
||||
/******/ __webpack_require__.m = modules;
|
||||
/******/
|
||||
/******/ // expose the module cache
|
||||
/******/ __webpack_require__.c = installedModules;
|
||||
/******/
|
||||
/******/ // define getter function for harmony exports
|
||||
/******/ __webpack_require__.d = function(exports, name, getter) {
|
||||
/******/ if(!__webpack_require__.o(exports, name)) {
|
||||
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
|
||||
/******/ }
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // define __esModule on exports
|
||||
/******/ __webpack_require__.r = function(exports) {
|
||||
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
||||
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
||||
/******/ }
|
||||
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // create a fake namespace object
|
||||
/******/ // mode & 1: value is a module id, require it
|
||||
/******/ // mode & 2: merge all properties of value into the ns
|
||||
/******/ // mode & 4: return value when already ns object
|
||||
/******/ // mode & 8|1: behave like require
|
||||
/******/ __webpack_require__.t = function(value, mode) {
|
||||
/******/ if(mode & 1) value = __webpack_require__(value);
|
||||
/******/ if(mode & 8) return value;
|
||||
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
|
||||
/******/ var ns = Object.create(null);
|
||||
/******/ __webpack_require__.r(ns);
|
||||
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
|
||||
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
|
||||
/******/ return ns;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
||||
/******/ __webpack_require__.n = function(module) {
|
||||
/******/ var getter = module && module.__esModule ?
|
||||
/******/ function getDefault() { return module['default']; } :
|
||||
/******/ function getModuleExports() { return module; };
|
||||
/******/ __webpack_require__.d(getter, 'a', getter);
|
||||
/******/ return getter;
|
||||
/******/ };
|
||||
/******/
|
||||
/******/ // Object.prototype.hasOwnProperty.call
|
||||
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
|
||||
/******/
|
||||
/******/ // __webpack_public_path__
|
||||
/******/ __webpack_require__.p = "";
|
||||
/******/
|
||||
/******/
|
||||
/******/ // Load entry module and return exports
|
||||
/******/ return __webpack_require__(__webpack_require__.s = "deee");
|
||||
/******/ })
|
||||
/************************************************************************/
|
||||
/******/ ({
|
||||
|
||||
/***/ "789d":
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
// extracted by mini-css-extract-plugin
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "b284":
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
/* harmony import */ var _plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("789d");
|
||||
/* harmony import */ var _plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0__);
|
||||
/* unused harmony reexport * */
|
||||
/* unused harmony default export */ var _unused_webpack_default_export = (_plugin_webrtc_ui_node_modules_mini_css_extract_plugin_dist_loader_js_ref_6_oneOf_1_0_plugin_webrtc_ui_node_modules_css_loader_dist_cjs_js_ref_6_oneOf_1_1_plugin_webrtc_ui_node_modules_vue_loader_lib_loaders_stylePostLoader_js_plugin_webrtc_ui_node_modules_postcss_loader_src_index_js_ref_6_oneOf_1_2_plugin_webrtc_ui_node_modules_cache_loader_dist_cjs_js_ref_0_0_plugin_webrtc_ui_node_modules_vue_loader_lib_index_js_vue_loader_options_Player_vue_vue_type_style_index_0_id_70987c50_scoped_true_lang_css___WEBPACK_IMPORTED_MODULE_0___default.a);
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "deee":
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
// ESM COMPAT FLAG
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/@vue/cli-service/lib/commands/build/setPublicPath.js
|
||||
// This file is imported into lib/wc client bundles.
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
var currentScript = window.document.currentScript
|
||||
if (true) {
|
||||
var getCurrentScript = __webpack_require__("f3a8")
|
||||
currentScript = getCurrentScript()
|
||||
|
||||
// for backward compatibility, because previously we directly included the polyfill
|
||||
if (!('currentScript' in document)) {
|
||||
Object.defineProperty(document, 'currentScript', { get: getCurrentScript })
|
||||
}
|
||||
}
|
||||
|
||||
var src = currentScript && currentScript.src.match(/(.+\/)[^/]+\.js(\?.*)?$/)
|
||||
if (src) {
|
||||
__webpack_require__.p = src[1] // eslint-disable-line
|
||||
}
|
||||
}
|
||||
|
||||
// Indicate to webpack that this file can be concatenated
|
||||
/* harmony default export */ var setPublicPath = (null);
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"2507526a-vue-loader-template"}!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=template&id=764a9e60&
|
||||
var render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',[_c('mu-data-table',{attrs:{"data":_vm.Devices,"columns":_vm.columns},scopedSlots:_vm._u([{key:"expand",fn:function(prop){return [_c('mu-data-table',{attrs:{"data":prop.row.Channels,"columns":_vm.columns2},scopedSlots:_vm._u([{key:"default",fn:function(ref){
|
||||
var item = ref.row;
|
||||
var $index = ref.$index;
|
||||
return [_c('td',[_vm._v(_vm._s(item.DeviceID))]),_c('td',[_vm._v(_vm._s(item.Name))]),_c('td',[_vm._v(_vm._s(item.Manufacturer))]),_c('td',[_vm._v(_vm._s(item.Address))]),_c('td',[_vm._v(_vm._s(item.Status))]),_c('td',[(item.Connected)?_c('mu-button',{attrs:{"flat":""},on:{"click":function($event){return _vm.ptz(prop.row.ID, $index,item)}}},[_vm._v("云台")]):_vm._e(),(item.Connected)?_c('mu-button',{attrs:{"flat":""},on:{"click":function($event){return _vm.bye(prop.row.ID, $index)}}},[_vm._v("断开")]):_c('mu-button',{attrs:{"flat":""},on:{"click":function($event){return _vm.invite(prop.row.ID, $index,item)}}},[_vm._v("连接 ")])],1)]}}],null,true)})]}},{key:"default",fn:function(ref){
|
||||
var item = ref.row;
|
||||
return [_c('td',[_vm._v(_vm._s(item.ID))]),_c('td',[_vm._v(_vm._s(item.Channels ? item.Channels.length : 0))]),_c('td',[_c('StartTime',{attrs:{"value":item.RegisterTime}})],1),_c('td',[_c('StartTime',{attrs:{"value":item.UpdateTime}})],1),_c('td',[_vm._v(_vm._s(item.Status))])]}}])}),_c('webrtc-player',{ref:"player",attrs:{"PublicIP":_vm.PublicIP},on:{"ptz":_vm.sendPtz},model:{value:(_vm.previewStreamPath),callback:function ($$v) {_vm.previewStreamPath=$$v},expression:"previewStreamPath"}})],1)}
|
||||
var staticRenderFns = []
|
||||
|
||||
|
||||
// CONCATENATED MODULE: ./src/App.vue?vue&type=template&id=764a9e60&
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js?{"cacheDirectory":"node_modules/.cache/vue-loader","cacheIdentifier":"2507526a-vue-loader-template"}!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib/loaders/templateLoader.js??vue-loader-options!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/components/Player.vue?vue&type=template&id=70987c50&scoped=true&
|
||||
var Playervue_type_template_id_70987c50_scoped_true_render = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('Modal',_vm._g(_vm._b({attrs:{"draggable":"","title":_vm.streamPath},on:{"on-ok":_vm.onClosePreview,"on-cancel":_vm.onClosePreview}},'Modal',_vm.$attrs,false),_vm.$listeners),[_c('div',{staticClass:"container"},[_c('video',{ref:"webrtc",attrs:{"width":"488","height":"275","autoplay":"","muted":"","controls":""},domProps:{"srcObject":_vm.stream,"muted":true}}),_c('div',{staticClass:"control"},_vm._l((4),function(n){return _c('svg',{class:'arrow'+n,attrs:{"viewBox":"0 0 1024 1024","version":"1.1","xmlns":"http://www.w3.org/2000/svg","xmlns:xlink":"http://www.w3.org/1999/xlink","width":"64","height":"64"},on:{"click":function($event){return _vm.$emit('ptz',n)}}},[_c('defs'),_c('path',{attrs:{"d":"M682.666667 955.733333H341.333333a17.066667 17.066667 0 0 1-17.066666-17.066666V529.066667H85.333333a17.066667 17.066667 0 0 1-12.066133-29.1328l426.666667-426.666667a17.0496 17.0496 0 0 1 24.132266 0l426.666667 426.666667A17.066667 17.066667 0 0 1 938.666667 529.066667H699.733333v409.6a17.066667 17.066667 0 0 1-17.066666 17.066666z m-324.266667-34.133333h307.2V512a17.066667 17.066667 0 0 1 17.066667-17.066667h214.801066L512 109.4656 126.532267 494.933333H341.333333a17.066667 17.066667 0 0 1 17.066667 17.066667v409.6z","p-id":"6849"}})])}),0)]),_c('div',{attrs:{"slot":"footer"},slot:"footer"},[(_vm.remoteSDP)?_c('mu-badge',[_c('a',{attrs:{"slot":"content","href":_vm.remoteSDPURL,"download":"remoteSDP.txt"},slot:"content"},[_vm._v("remoteSDP")])]):_vm._e(),(_vm.localSDP)?_c('mu-badge',[_c('a',{attrs:{"slot":"content","href":_vm.localSDPURL,"download":"localSDP.txt"},slot:"content"},[_vm._v("localSDP")])]):_vm._e()],1)])}
|
||||
var Playervue_type_template_id_70987c50_scoped_true_staticRenderFns = []
|
||||
|
||||
|
||||
// CONCATENATED MODULE: ./src/components/Player.vue?vue&type=template&id=70987c50&scoped=true&
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/components/Player.vue?vue&type=script&lang=js&
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
let pc = null;
|
||||
/* harmony default export */ var Playervue_type_script_lang_js_ = ({
|
||||
data() {
|
||||
return {
|
||||
iceConnectionState: pc && pc.iceConnectionState,
|
||||
stream: null,
|
||||
localSDP: "",
|
||||
remoteSDP: "",
|
||||
remoteSDPURL: "",
|
||||
localSDPURL: "",
|
||||
streamPath: ""
|
||||
};
|
||||
},
|
||||
props:{
|
||||
PublicIP:String
|
||||
},
|
||||
methods: {
|
||||
async play(streamPath) {
|
||||
pc = new RTCPeerConnection();
|
||||
pc.addTransceiver('video',{
|
||||
direction:'recvonly'
|
||||
})
|
||||
this.streamPath = streamPath;
|
||||
pc.onsignalingstatechange = e => {
|
||||
//console.log(e);
|
||||
};
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
this.$toast.info(pc.iceConnectionState);
|
||||
this.iceConnectionState = pc.iceConnectionState;
|
||||
};
|
||||
pc.onicecandidate = event => {
|
||||
console.log(event)
|
||||
};
|
||||
pc.ontrack = event => {
|
||||
// console.log(event);
|
||||
if (event.track.kind == "video")
|
||||
this.stream = event.streams[0];
|
||||
};
|
||||
await pc.setLocalDescription(await pc.createOffer());
|
||||
this.localSDP = pc.localDescription.sdp;
|
||||
this.localSDPURL = URL.createObjectURL(
|
||||
new Blob([this.localSDP], { type: "text/plain" })
|
||||
);
|
||||
const result = await this.ajax({
|
||||
type: "POST",
|
||||
processData: false,
|
||||
data: JSON.stringify(pc.localDescription.toJSON()),
|
||||
url: "/webrtc/play?streamPath=" + this.streamPath,
|
||||
dataType: "json"
|
||||
});
|
||||
if (result.errmsg) {
|
||||
this.$toast.error(result.errmsg);
|
||||
return;
|
||||
} else {
|
||||
this.remoteSDP = result.sdp;
|
||||
this.remoteSDPURL = URL.createObjectURL(new Blob([this.remoteSDP], { type: "text/plain" }));
|
||||
}
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(result));
|
||||
},
|
||||
onClosePreview() {
|
||||
pc.close();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// CONCATENATED MODULE: ./src/components/Player.vue?vue&type=script&lang=js&
|
||||
/* harmony default export */ var components_Playervue_type_script_lang_js_ = (Playervue_type_script_lang_js_);
|
||||
// EXTERNAL MODULE: ./src/components/Player.vue?vue&type=style&index=0&id=70987c50&scoped=true&lang=css&
|
||||
var Playervue_type_style_index_0_id_70987c50_scoped_true_lang_css_ = __webpack_require__("b284");
|
||||
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib/runtime/componentNormalizer.js
|
||||
/* globals __VUE_SSR_CONTEXT__ */
|
||||
|
||||
// IMPORTANT: Do NOT use ES2015 features in this file (except for modules).
|
||||
// This module is a runtime utility for cleaner component module output and will
|
||||
// be included in the final webpack user bundle.
|
||||
|
||||
function normalizeComponent (
|
||||
scriptExports,
|
||||
render,
|
||||
staticRenderFns,
|
||||
functionalTemplate,
|
||||
injectStyles,
|
||||
scopeId,
|
||||
moduleIdentifier, /* server only */
|
||||
shadowMode /* vue-cli only */
|
||||
) {
|
||||
// Vue.extend constructor export interop
|
||||
var options = typeof scriptExports === 'function'
|
||||
? scriptExports.options
|
||||
: scriptExports
|
||||
|
||||
// render functions
|
||||
if (render) {
|
||||
options.render = render
|
||||
options.staticRenderFns = staticRenderFns
|
||||
options._compiled = true
|
||||
}
|
||||
|
||||
// functional template
|
||||
if (functionalTemplate) {
|
||||
options.functional = true
|
||||
}
|
||||
|
||||
// scopedId
|
||||
if (scopeId) {
|
||||
options._scopeId = 'data-v-' + scopeId
|
||||
}
|
||||
|
||||
var hook
|
||||
if (moduleIdentifier) { // server build
|
||||
hook = function (context) {
|
||||
// 2.3 injection
|
||||
context =
|
||||
context || // cached call
|
||||
(this.$vnode && this.$vnode.ssrContext) || // stateful
|
||||
(this.parent && this.parent.$vnode && this.parent.$vnode.ssrContext) // functional
|
||||
// 2.2 with runInNewContext: true
|
||||
if (!context && typeof __VUE_SSR_CONTEXT__ !== 'undefined') {
|
||||
context = __VUE_SSR_CONTEXT__
|
||||
}
|
||||
// inject component styles
|
||||
if (injectStyles) {
|
||||
injectStyles.call(this, context)
|
||||
}
|
||||
// register component module identifier for async chunk inferrence
|
||||
if (context && context._registeredComponents) {
|
||||
context._registeredComponents.add(moduleIdentifier)
|
||||
}
|
||||
}
|
||||
// used by ssr in case component is cached and beforeCreate
|
||||
// never gets called
|
||||
options._ssrRegister = hook
|
||||
} else if (injectStyles) {
|
||||
hook = shadowMode
|
||||
? function () {
|
||||
injectStyles.call(
|
||||
this,
|
||||
(options.functional ? this.parent : this).$root.$options.shadowRoot
|
||||
)
|
||||
}
|
||||
: injectStyles
|
||||
}
|
||||
|
||||
if (hook) {
|
||||
if (options.functional) {
|
||||
// for template-only hot-reload because in that case the render fn doesn't
|
||||
// go through the normalizer
|
||||
options._injectStyles = hook
|
||||
// register for functional component in vue file
|
||||
var originalRender = options.render
|
||||
options.render = function renderWithStyleInjection (h, context) {
|
||||
hook.call(context)
|
||||
return originalRender(h, context)
|
||||
}
|
||||
} else {
|
||||
// inject component registration as beforeCreate hook
|
||||
var existing = options.beforeCreate
|
||||
options.beforeCreate = existing
|
||||
? [].concat(existing, hook)
|
||||
: [hook]
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
exports: scriptExports,
|
||||
options: options
|
||||
}
|
||||
}
|
||||
|
||||
// CONCATENATED MODULE: ./src/components/Player.vue
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* normalize component */
|
||||
|
||||
var component = normalizeComponent(
|
||||
components_Playervue_type_script_lang_js_,
|
||||
Playervue_type_template_id_70987c50_scoped_true_render,
|
||||
Playervue_type_template_id_70987c50_scoped_true_staticRenderFns,
|
||||
false,
|
||||
null,
|
||||
"70987c50",
|
||||
null
|
||||
|
||||
)
|
||||
|
||||
/* harmony default export */ var Player = (component.exports);
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/cache-loader/dist/cjs.js??ref--0-0!C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/vue-loader/lib??vue-loader-options!./src/App.vue?vue&type=script&lang=js&
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
//
|
||||
|
||||
|
||||
/* harmony default export */ var Appvue_type_script_lang_js_ = ({
|
||||
components:{
|
||||
WebrtcPlayer: Player
|
||||
},
|
||||
props:{
|
||||
ListenAddr:String
|
||||
},
|
||||
computed:{
|
||||
PublicIP(){
|
||||
return this.ListenAddr.split(":")[0]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Devices: [], previewStreamPath:false,
|
||||
context:{
|
||||
id:null,
|
||||
channel:0,
|
||||
item:null
|
||||
},
|
||||
columns: Object.freeze(
|
||||
["设备号", "通道数", "注册时间", "更新时间", "状态"].map(
|
||||
(title) => ({
|
||||
title,
|
||||
})
|
||||
)
|
||||
),
|
||||
columns2: Object.freeze([
|
||||
"通道编号",
|
||||
"名称",
|
||||
"厂商",
|
||||
"地址",
|
||||
"状态",
|
||||
"操作",
|
||||
]).map((title) => ({title})),
|
||||
ptzCmds:["A50F010800880045","A50F01018800003E", "A50F010400880041","A50F01028800003F"]
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchlist();
|
||||
},
|
||||
methods: {
|
||||
fetchlist() {
|
||||
const listES = new EventSource(this.apiHost + "/gb28181/list");
|
||||
listES.onmessage = (evt) => {
|
||||
if (!evt.data) return;
|
||||
this.Devices = JSON.parse(evt.data) || [];
|
||||
this.Devices.sort((a, b) => (a.ID > b.ID ? 1 : -1));
|
||||
};
|
||||
this.$once("hook:destroyed", () => listES.close());
|
||||
},
|
||||
ptz(id, channel,item) {
|
||||
this.context = {
|
||||
id,channel,item
|
||||
}
|
||||
this.previewStreamPath = true
|
||||
this.$nextTick(() =>this.$refs.player.play("gb28181/"+item.DeviceID));
|
||||
},
|
||||
sendPtz(n){
|
||||
this.ajax.get("/gb28181/control", {
|
||||
id:this.context.id,
|
||||
channel:this.context.channel,
|
||||
ptzcmd: this.ptzCmds[n-1],
|
||||
}).then(x=>{
|
||||
setTimeout(()=>{
|
||||
this.ajax.get("/gb28181/control", {
|
||||
id:this.context.id,
|
||||
channel:this.context.channel,
|
||||
ptzcmd: "A50F0100000000B5",
|
||||
});
|
||||
},500)
|
||||
});
|
||||
},
|
||||
invite(id, channel,item) {
|
||||
this.ajax.get("/gb28181/invite", {id, channel}).then(x=>{
|
||||
item.Connected = true
|
||||
});
|
||||
},
|
||||
bye(id, channel,item) {
|
||||
this.ajax.get("/gb28181/bye", {id, channel}).then(x=>{
|
||||
item.Connected = false
|
||||
});;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// CONCATENATED MODULE: ./src/App.vue?vue&type=script&lang=js&
|
||||
/* harmony default export */ var src_Appvue_type_script_lang_js_ = (Appvue_type_script_lang_js_);
|
||||
// CONCATENATED MODULE: ./src/App.vue
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/* normalize component */
|
||||
|
||||
var App_component = normalizeComponent(
|
||||
src_Appvue_type_script_lang_js_,
|
||||
render,
|
||||
staticRenderFns,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
null
|
||||
|
||||
)
|
||||
|
||||
/* harmony default export */ var App = (App_component.exports);
|
||||
// CONCATENATED MODULE: C:/Users/dexte/go/src/github.com/Monibuca/plugin-webrtc/ui/node_modules/@vue/cli-service/lib/commands/build/entry-lib.js
|
||||
|
||||
|
||||
/* harmony default export */ var entry_lib = __webpack_exports__["default"] = (App);
|
||||
|
||||
|
||||
|
||||
/***/ }),
|
||||
|
||||
/***/ "f3a8":
|
||||
/***/ (function(module, exports, __webpack_require__) {
|
||||
|
||||
var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;// addapted from the document.currentScript polyfill by Adam Miller
|
||||
// MIT license
|
||||
// source: https://github.com/amiller-gh/currentScript-polyfill
|
||||
|
||||
// added support for Firefox https://bugzilla.mozilla.org/show_bug.cgi?id=1620505
|
||||
|
||||
(function (root, factory) {
|
||||
if (true) {
|
||||
!(__WEBPACK_AMD_DEFINE_ARRAY__ = [], __WEBPACK_AMD_DEFINE_FACTORY__ = (factory),
|
||||
__WEBPACK_AMD_DEFINE_RESULT__ = (typeof __WEBPACK_AMD_DEFINE_FACTORY__ === 'function' ?
|
||||
(__WEBPACK_AMD_DEFINE_FACTORY__.apply(exports, __WEBPACK_AMD_DEFINE_ARRAY__)) : __WEBPACK_AMD_DEFINE_FACTORY__),
|
||||
__WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
|
||||
} else {}
|
||||
}(typeof self !== 'undefined' ? self : this, function () {
|
||||
function getCurrentScript () {
|
||||
if (document.currentScript) {
|
||||
return document.currentScript
|
||||
}
|
||||
|
||||
// IE 8-10 support script readyState
|
||||
// IE 11+ & Firefox support stack trace
|
||||
try {
|
||||
throw new Error();
|
||||
}
|
||||
catch (err) {
|
||||
// Find the second match for the "at" string to get file src url from stack.
|
||||
var ieStackRegExp = /.*at [^(]*\((.*):(.+):(.+)\)$/ig,
|
||||
ffStackRegExp = /@([^@]*):(\d+):(\d+)\s*$/ig,
|
||||
stackDetails = ieStackRegExp.exec(err.stack) || ffStackRegExp.exec(err.stack),
|
||||
scriptLocation = (stackDetails && stackDetails[1]) || false,
|
||||
line = (stackDetails && stackDetails[2]) || false,
|
||||
currentLocation = document.location.href.replace(document.location.hash, ''),
|
||||
pageSource,
|
||||
inlineScriptSourceRegExp,
|
||||
inlineScriptSource,
|
||||
scripts = document.getElementsByTagName('script'); // Live NodeList collection
|
||||
|
||||
if (scriptLocation === currentLocation) {
|
||||
pageSource = document.documentElement.outerHTML;
|
||||
inlineScriptSourceRegExp = new RegExp('(?:[^\\n]+?\\n){0,' + (line - 2) + '}[^<]*<script>([\\d\\D]*?)<\\/script>[\\d\\D]*', 'i');
|
||||
inlineScriptSource = pageSource.replace(inlineScriptSourceRegExp, '$1').trim();
|
||||
}
|
||||
|
||||
for (var i = 0; i < scripts.length; i++) {
|
||||
// If ready state is interactive, return the script tag
|
||||
if (scripts[i].readyState === 'interactive') {
|
||||
return scripts[i];
|
||||
}
|
||||
|
||||
// If src matches, return the script tag
|
||||
if (scripts[i].src === scriptLocation) {
|
||||
return scripts[i];
|
||||
}
|
||||
|
||||
// If inline source matches, return the script tag
|
||||
if (
|
||||
scriptLocation === currentLocation &&
|
||||
scripts[i].innerHTML &&
|
||||
scripts[i].innerHTML.trim() === inlineScriptSource
|
||||
) {
|
||||
return scripts[i];
|
||||
}
|
||||
}
|
||||
|
||||
// If no match, return null
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return getCurrentScript
|
||||
}));
|
||||
|
||||
|
||||
/***/ })
|
||||
|
||||
/******/ })["default"];
|
||||
});
|
||||
//# sourceMappingURL=plugin-gb28181.umd.js.map
|
||||
1
ui/dist/plugin-gb28181.umd.js.map
vendored
1
ui/dist/plugin-gb28181.umd.js.map
vendored
File diff suppressed because one or more lines are too long
2
ui/dist/plugin-gb28181.umd.min.js
vendored
2
ui/dist/plugin-gb28181.umd.min.js
vendored
File diff suppressed because one or more lines are too long
1
ui/dist/plugin-gb28181.umd.min.js.map
vendored
1
ui/dist/plugin-gb28181.umd.min.js.map
vendored
File diff suppressed because one or more lines are too long
9373
ui/package-lock.json
generated
9373
ui/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "dashboard",
|
||||
"version": "1.0.0",
|
||||
"description": "dashboard of gb28181 plugin for monibuca",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"build": "vue-cli-service build --target lib --name plugin-gb28181"
|
||||
},
|
||||
"author": "dexter",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@vue/cli-service": "^4.5.4",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
}
|
||||
}
|
||||
125
ui/src/App.vue
125
ui/src/App.vue
@@ -1,125 +0,0 @@
|
||||
<template>
|
||||
<div>
|
||||
<mu-data-table :data="Devices" :columns="columns">
|
||||
<template #expand="prop">
|
||||
<mu-data-table :data="prop.row.Channels" :columns="columns2">
|
||||
<template #default="{ row: item, $index }">
|
||||
<td>{{ item.DeviceID }}</td>
|
||||
<td>{{ item.Name }}</td>
|
||||
<td>{{ item.Manufacturer }}</td>
|
||||
<td>{{ item.Address }}</td>
|
||||
<td>{{ item.Status }}</td>
|
||||
<td>
|
||||
<mu-button flat v-if="item.Connected" @click="ptz(prop.row.ID, $index,item)">云台</mu-button>
|
||||
<mu-button flat v-if="item.Connected" @click="bye(prop.row.ID, $index)">断开</mu-button>
|
||||
<mu-button v-else flat @click="invite(prop.row.ID, $index,item)"
|
||||
>连接
|
||||
</mu-button
|
||||
>
|
||||
</td>
|
||||
</template>
|
||||
</mu-data-table>
|
||||
</template>
|
||||
<template #default="{ row: item }">
|
||||
<td>{{ item.ID }}</td>
|
||||
<td>{{ item.Channels ? item.Channels.length : 0 }}</td>
|
||||
<td>
|
||||
<StartTime :value="item.RegisterTime"></StartTime>
|
||||
</td>
|
||||
<td>
|
||||
<StartTime :value="item.UpdateTime"></StartTime>
|
||||
</td>
|
||||
<td>{{ item.Status }}</td>
|
||||
</template>
|
||||
</mu-data-table>
|
||||
<webrtc-player ref="player" @ptz="sendPtz" v-model="previewStreamPath" :PublicIP="PublicIP"></webrtc-player>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import WebrtcPlayer from "./components/Player"
|
||||
export default {
|
||||
components:{
|
||||
WebrtcPlayer
|
||||
},
|
||||
props:{
|
||||
ListenAddr:String
|
||||
},
|
||||
computed:{
|
||||
PublicIP(){
|
||||
return this.ListenAddr.split(":")[0]
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
Devices: [], previewStreamPath:false,
|
||||
context:{
|
||||
id:null,
|
||||
channel:0,
|
||||
item:null
|
||||
},
|
||||
columns: Object.freeze(
|
||||
["设备号", "通道数", "注册时间", "更新时间", "状态"].map(
|
||||
(title) => ({
|
||||
title,
|
||||
})
|
||||
)
|
||||
),
|
||||
columns2: Object.freeze([
|
||||
"通道编号",
|
||||
"名称",
|
||||
"厂商",
|
||||
"地址",
|
||||
"状态",
|
||||
"操作",
|
||||
]).map((title) => ({title})),
|
||||
ptzCmds:["A50F010800880045","A50F01018800003E", "A50F010400880041","A50F01028800003F"]
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.fetchlist();
|
||||
},
|
||||
methods: {
|
||||
fetchlist() {
|
||||
const listES = new EventSource(this.apiHost + "/gb28181/list");
|
||||
listES.onmessage = (evt) => {
|
||||
if (!evt.data) return;
|
||||
this.Devices = JSON.parse(evt.data) || [];
|
||||
this.Devices.sort((a, b) => (a.ID > b.ID ? 1 : -1));
|
||||
};
|
||||
this.$once("hook:destroyed", () => listES.close());
|
||||
},
|
||||
ptz(id, channel,item) {
|
||||
this.context = {
|
||||
id,channel,item
|
||||
}
|
||||
this.previewStreamPath = true
|
||||
this.$nextTick(() =>this.$refs.player.play("gb28181/"+item.DeviceID));
|
||||
},
|
||||
sendPtz(n){
|
||||
this.ajax.get("/gb28181/control", {
|
||||
id:this.context.id,
|
||||
channel:this.context.channel,
|
||||
ptzcmd: this.ptzCmds[n-1],
|
||||
}).then(x=>{
|
||||
setTimeout(()=>{
|
||||
this.ajax.get("/gb28181/control", {
|
||||
id:this.context.id,
|
||||
channel:this.context.channel,
|
||||
ptzcmd: "A50F0100000000B5",
|
||||
});
|
||||
},500)
|
||||
});
|
||||
},
|
||||
invite(id, channel,item) {
|
||||
this.ajax.get("/gb28181/invite", {id, channel}).then(x=>{
|
||||
item.Connected = true
|
||||
});
|
||||
},
|
||||
bye(id, channel,item) {
|
||||
this.ajax.get("/gb28181/bye", {id, channel}).then(x=>{
|
||||
item.Connected = false
|
||||
});;
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,132 +0,0 @@
|
||||
<template>
|
||||
<Modal
|
||||
v-bind="$attrs"
|
||||
draggable
|
||||
v-on="$listeners"
|
||||
:title="streamPath"
|
||||
@on-ok="onClosePreview"
|
||||
@on-cancel="onClosePreview"
|
||||
>
|
||||
<div class="container">
|
||||
<video ref="webrtc" :srcObject.prop="stream" width="488" height="275" autoplay muted controls></video>
|
||||
<div class="control">
|
||||
<svg @click="$emit('ptz',n)" v-for="n in 4" :class="'arrow'+n" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="64" height="64"><defs><style type="text/css"></style></defs><path d="M682.666667 955.733333H341.333333a17.066667 17.066667 0 0 1-17.066666-17.066666V529.066667H85.333333a17.066667 17.066667 0 0 1-12.066133-29.1328l426.666667-426.666667a17.0496 17.0496 0 0 1 24.132266 0l426.666667 426.666667A17.066667 17.066667 0 0 1 938.666667 529.066667H699.733333v409.6a17.066667 17.066667 0 0 1-17.066666 17.066666z m-324.266667-34.133333h307.2V512a17.066667 17.066667 0 0 1 17.066667-17.066667h214.801066L512 109.4656 126.532267 494.933333H341.333333a17.066667 17.066667 0 0 1 17.066667 17.066667v409.6z" p-id="6849"></path></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div slot="footer">
|
||||
<mu-badge v-if="remoteSDP">
|
||||
<a slot="content" :href="remoteSDPURL" download="remoteSDP.txt">remoteSDP</a>
|
||||
</mu-badge>
|
||||
<mu-badge v-if="localSDP">
|
||||
<a slot="content" :href="localSDPURL" download="localSDP.txt">localSDP</a>
|
||||
</mu-badge>
|
||||
</div>
|
||||
</Modal>
|
||||
</template>
|
||||
<script>
|
||||
let pc = null;
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
iceConnectionState: pc && pc.iceConnectionState,
|
||||
stream: null,
|
||||
localSDP: "",
|
||||
remoteSDP: "",
|
||||
remoteSDPURL: "",
|
||||
localSDPURL: "",
|
||||
streamPath: ""
|
||||
};
|
||||
},
|
||||
props:{
|
||||
PublicIP:String
|
||||
},
|
||||
methods: {
|
||||
async play(streamPath) {
|
||||
pc = new RTCPeerConnection();
|
||||
pc.addTransceiver('video',{
|
||||
direction:'recvonly'
|
||||
})
|
||||
this.streamPath = streamPath;
|
||||
pc.onsignalingstatechange = e => {
|
||||
//console.log(e);
|
||||
};
|
||||
pc.oniceconnectionstatechange = e => {
|
||||
this.$toast.info(pc.iceConnectionState);
|
||||
this.iceConnectionState = pc.iceConnectionState;
|
||||
};
|
||||
pc.onicecandidate = event => {
|
||||
console.log(event)
|
||||
};
|
||||
pc.ontrack = event => {
|
||||
// console.log(event);
|
||||
if (event.track.kind == "video")
|
||||
this.stream = event.streams[0];
|
||||
};
|
||||
await pc.setLocalDescription(await pc.createOffer());
|
||||
this.localSDP = pc.localDescription.sdp;
|
||||
this.localSDPURL = URL.createObjectURL(
|
||||
new Blob([this.localSDP], { type: "text/plain" })
|
||||
);
|
||||
const result = await this.ajax({
|
||||
type: "POST",
|
||||
processData: false,
|
||||
data: JSON.stringify(pc.localDescription.toJSON()),
|
||||
url: "/webrtc/play?streamPath=" + this.streamPath,
|
||||
dataType: "json"
|
||||
});
|
||||
if (result.errmsg) {
|
||||
this.$toast.error(result.errmsg);
|
||||
return;
|
||||
} else {
|
||||
this.remoteSDP = result.sdp;
|
||||
this.remoteSDPURL = URL.createObjectURL(new Blob([this.remoteSDP], { type: "text/plain" }));
|
||||
}
|
||||
await pc.setRemoteDescription(new RTCSessionDescription(result));
|
||||
},
|
||||
onClosePreview() {
|
||||
pc.close();
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.arrow1{
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
.arrow2{
|
||||
transform: rotate(90deg);
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
.arrow3{
|
||||
transform: rotate(180deg);
|
||||
grid-column: 2;
|
||||
grid-row: 3;
|
||||
}
|
||||
.arrow4{
|
||||
transform: rotate(270deg);
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
.container {
|
||||
position: relative;
|
||||
}
|
||||
.control {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 33.33%);
|
||||
grid-template-rows: repeat(3, 33.33%);
|
||||
width: 192px;
|
||||
height: 192px;
|
||||
}
|
||||
.control >* {
|
||||
cursor: pointer;
|
||||
fill: gray;
|
||||
}
|
||||
.control >*:hover{
|
||||
fill: cyan
|
||||
}
|
||||
</style>
|
||||
112
utils/buffer.go
Normal file
112
utils/buffer.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
)
|
||||
|
||||
type IOBuffer struct {
|
||||
buf []byte // contents are the bytes buf[off : len(buf)]
|
||||
off int // read at &buf[off], write at &buf[len(buf)]
|
||||
}
|
||||
|
||||
func (b *IOBuffer) Next(n int) []byte {
|
||||
m := b.Len()
|
||||
if n > m {
|
||||
n = m
|
||||
}
|
||||
data := b.buf[b.off : b.off+n]
|
||||
b.off += n
|
||||
return data
|
||||
}
|
||||
func (b *IOBuffer) Uint16() (uint16, error) {
|
||||
if b.Len() > 1 {
|
||||
|
||||
return binary.BigEndian.Uint16(b.Next(2)), nil
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (b *IOBuffer) Skip(n int) (err error) {
|
||||
_, err = b.ReadN(n)
|
||||
return
|
||||
}
|
||||
|
||||
func (b *IOBuffer) Uint32() (uint32, error) {
|
||||
if b.Len() > 3 {
|
||||
return binary.BigEndian.Uint32(b.Next(4)), nil
|
||||
}
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
func (b *IOBuffer) ReadN(length int) ([]byte, error) {
|
||||
if b.Len() >= length {
|
||||
return b.Next(length), nil
|
||||
}
|
||||
return nil, io.EOF
|
||||
}
|
||||
|
||||
//func (b *IOBuffer) Read(buf []byte) (n int, err error) {
|
||||
// var ret []byte
|
||||
// ret, err = b.ReadN(len(buf))
|
||||
// copy(buf, ret)
|
||||
// return len(ret), err
|
||||
//}
|
||||
|
||||
// empty reports whether the unread portion of the buffer is empty.
|
||||
func (b *IOBuffer) empty() bool { return b.Len() <= b.off }
|
||||
|
||||
func (b *IOBuffer) ReadByte() (byte, error) {
|
||||
if b.empty() {
|
||||
// Buffer is empty, reset to recover space.
|
||||
b.Reset()
|
||||
return 0, io.EOF
|
||||
}
|
||||
c := b.buf[b.off]
|
||||
b.off++
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (b *IOBuffer) Reset() {
|
||||
b.buf = b.buf[:0]
|
||||
b.off = 0
|
||||
}
|
||||
|
||||
func (b *IOBuffer) Len() int { return len(b.buf) - b.off }
|
||||
|
||||
// tryGrowByReslice is a inlineable version of grow for the fast-case where the
|
||||
// internal buffer only needs to be resliced.
|
||||
// It returns the index where bytes should be written and whether it succeeded.
|
||||
func (b *IOBuffer) tryGrowByReslice(n int) (int, bool) {
|
||||
if l := len(b.buf); n <= cap(b.buf)-l {
|
||||
b.buf = b.buf[:l+n]
|
||||
return l, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
var ErrTooLarge = errors.New("IOBuffer: too large")
|
||||
|
||||
func (b *IOBuffer) Write(p []byte) (n int, err error) {
|
||||
l := copy(b.buf, b.buf[b.off:])
|
||||
b.buf = append(b.buf[:l], p...)
|
||||
b.off = 0
|
||||
// println(b.buf, b.off, b.buf[b.off], b.buf[b.off+1], b.buf[b.off+2], b.buf[b.off+3])
|
||||
return len(p), nil
|
||||
// defer func() {
|
||||
// if recover() != nil {
|
||||
// panic(ErrTooLarge)
|
||||
// }
|
||||
// }()
|
||||
// l := len(p)
|
||||
// oldLen := len(b.buf)
|
||||
// m, ok := b.tryGrowByReslice(l)
|
||||
// if !ok {
|
||||
// m = oldLen - b.off
|
||||
// buf := append(append(([]byte)(nil), b.buf[b.off:]...), p...)
|
||||
// b.off = 0
|
||||
// b.buf = buf
|
||||
// }
|
||||
// return copy(b.buf[m:], p), nil
|
||||
}
|
||||
150
utils/bufferpool.go
Normal file
150
utils/bufferpool.go
Normal file
@@ -0,0 +1,150 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sort"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
minBitSize = 6 // 2**6=64 is a CPU cache line size
|
||||
steps = 20
|
||||
|
||||
minSize = 1 << minBitSize
|
||||
maxSize = 1 << (minBitSize + steps - 1)
|
||||
|
||||
calibrateCallsThreshold = 42000
|
||||
maxPercentile = 0.95
|
||||
)
|
||||
|
||||
// Pool represents byte buffer pool.
|
||||
//
|
||||
// Distinct pools may be used for distinct types of byte buffers.
|
||||
// Properly determined byte buffer types with their own pools may help reducing
|
||||
// memory waste.
|
||||
type Pool struct {
|
||||
calls [steps]uint64
|
||||
calibrating uint64
|
||||
|
||||
defaultSize uint64
|
||||
maxSize uint64
|
||||
|
||||
pool sync.Pool
|
||||
}
|
||||
|
||||
var defaultPool Pool
|
||||
|
||||
// Get returns an empty byte buffer from the pool.
|
||||
//
|
||||
// Got byte buffer may be returned to the pool via Put call.
|
||||
// This reduces the number of memory allocations required for byte buffer
|
||||
// management.
|
||||
func Get() *bytes.Buffer { return defaultPool.Get() }
|
||||
|
||||
// Get returns new byte buffer with zero length.
|
||||
//
|
||||
// The byte buffer may be returned to the pool via Put after the use
|
||||
// in order to minimize GC overhead.
|
||||
func (p *Pool) Get() *bytes.Buffer {
|
||||
v := p.pool.Get()
|
||||
if v != nil {
|
||||
return v.(*bytes.Buffer)
|
||||
}
|
||||
return bytes.NewBuffer(make([]byte, 0, atomic.LoadUint64(&p.defaultSize)))
|
||||
}
|
||||
|
||||
// Put returns byte buffer to the pool.
|
||||
//
|
||||
// bytes.Buffer.B mustn't be touched after returning it to the pool.
|
||||
// Otherwise data races will occur.
|
||||
func Put(b *bytes.Buffer) { b.Reset(); defaultPool.Put(b) }
|
||||
|
||||
// Put releases byte buffer obtained via Get to the pool.
|
||||
//
|
||||
// The buffer mustn't be accessed after returning to the pool.
|
||||
func (p *Pool) Put(b *bytes.Buffer) {
|
||||
idx := index(b.Len())
|
||||
|
||||
if atomic.AddUint64(&p.calls[idx], 1) > calibrateCallsThreshold {
|
||||
p.calibrate()
|
||||
}
|
||||
|
||||
maxSize := int(atomic.LoadUint64(&p.maxSize))
|
||||
if maxSize == 0 || b.Cap() <= maxSize {
|
||||
b.Reset()
|
||||
p.pool.Put(b)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pool) calibrate() {
|
||||
if !atomic.CompareAndSwapUint64(&p.calibrating, 0, 1) {
|
||||
return
|
||||
}
|
||||
|
||||
a := make(callSizes, 0, steps)
|
||||
var callsSum uint64
|
||||
for i := uint64(0); i < steps; i++ {
|
||||
calls := atomic.SwapUint64(&p.calls[i], 0)
|
||||
callsSum += calls
|
||||
a = append(a, callSize{
|
||||
calls: calls,
|
||||
size: minSize << i,
|
||||
})
|
||||
}
|
||||
sort.Sort(a)
|
||||
|
||||
defaultSize := a[0].size
|
||||
maxSize := defaultSize
|
||||
|
||||
maxSum := uint64(float64(callsSum) * maxPercentile)
|
||||
callsSum = 0
|
||||
for i := 0; i < steps; i++ {
|
||||
if callsSum > maxSum {
|
||||
break
|
||||
}
|
||||
callsSum += a[i].calls
|
||||
size := a[i].size
|
||||
if size > maxSize {
|
||||
maxSize = size
|
||||
}
|
||||
}
|
||||
|
||||
atomic.StoreUint64(&p.defaultSize, defaultSize)
|
||||
atomic.StoreUint64(&p.maxSize, maxSize)
|
||||
|
||||
atomic.StoreUint64(&p.calibrating, 0)
|
||||
}
|
||||
|
||||
type callSize struct {
|
||||
calls uint64
|
||||
size uint64
|
||||
}
|
||||
|
||||
type callSizes []callSize
|
||||
|
||||
func (ci callSizes) Len() int {
|
||||
return len(ci)
|
||||
}
|
||||
|
||||
func (ci callSizes) Less(i, j int) bool {
|
||||
return ci[i].calls > ci[j].calls
|
||||
}
|
||||
|
||||
func (ci callSizes) Swap(i, j int) {
|
||||
ci[i], ci[j] = ci[j], ci[i]
|
||||
}
|
||||
|
||||
func index(n int) int {
|
||||
n--
|
||||
n >>= minBitSize
|
||||
idx := 0
|
||||
for n > 0 {
|
||||
n >>= 1
|
||||
idx++
|
||||
}
|
||||
if idx >= steps {
|
||||
idx = steps - 1
|
||||
}
|
||||
return idx
|
||||
}
|
||||
159
utils/log.go
Executable file
159
utils/log.go
Executable file
@@ -0,0 +1,159 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ghettovoice/gosip/log"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
m7slog "m7s.live/engine/v4/log"
|
||||
)
|
||||
|
||||
type ZapLogger struct {
|
||||
log *m7slog.Logger
|
||||
prefix string
|
||||
fields log.Fields
|
||||
sugared *zap.SugaredLogger
|
||||
level log.Level
|
||||
}
|
||||
|
||||
func NewZapLogger(log *m7slog.Logger, prefix string, fields log.Fields) (z *ZapLogger) {
|
||||
z = &ZapLogger{
|
||||
log: log,
|
||||
prefix: prefix,
|
||||
fields: fields,
|
||||
}
|
||||
z.sugared = z.prepareEntry()
|
||||
return
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Print(args ...interface{}) {
|
||||
if l.level >= log.InfoLevel {
|
||||
l.sugared.Info(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Printf(format string, args ...interface{}) {
|
||||
if l.level >= log.InfoLevel {
|
||||
l.sugared.Infof(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Trace(args ...interface{}) {
|
||||
if l.level >= log.TraceLevel {
|
||||
l.sugared.Debug(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Tracef(format string, args ...interface{}) {
|
||||
if l.level >= log.TraceLevel {
|
||||
l.sugared.Debugf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Debug(args ...interface{}) {
|
||||
if l.level >= log.DebugLevel {
|
||||
l.sugared.Debug(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Debugf(format string, args ...interface{}) {
|
||||
if l.level >= log.DebugLevel {
|
||||
l.sugared.Debugf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Info(args ...interface{}) {
|
||||
if l.level >= log.InfoLevel {
|
||||
l.sugared.Info(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Infof(format string, args ...interface{}) {
|
||||
if l.level >= log.InfoLevel {
|
||||
l.sugared.Infof(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Warn(args ...interface{}) {
|
||||
if l.level >= log.WarnLevel {
|
||||
l.sugared.Warn(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Warnf(format string, args ...interface{}) {
|
||||
if l.level >= log.WarnLevel {
|
||||
l.sugared.Warnf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Error(args ...interface{}) {
|
||||
if l.level >= log.ErrorLevel {
|
||||
l.sugared.Error(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Errorf(format string, args ...interface{}) {
|
||||
if l.level >= log.ErrorLevel {
|
||||
l.sugared.Errorf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Fatal(args ...interface{}) {
|
||||
if l.level >= log.FatalLevel {
|
||||
l.sugared.Fatal(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Fatalf(format string, args ...interface{}) {
|
||||
if l.level >= log.FatalLevel {
|
||||
l.sugared.Fatalf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Panic(args ...interface{}) {
|
||||
if l.level >= log.PanicLevel {
|
||||
l.sugared.Panic(args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Panicf(format string, args ...interface{}) {
|
||||
if l.level >= log.PanicLevel {
|
||||
l.sugared.Panicf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
func (l *ZapLogger) WithPrefix(prefix string) log.Logger {
|
||||
return NewZapLogger(l.log, prefix, l.Fields())
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Prefix() string {
|
||||
return l.prefix
|
||||
}
|
||||
|
||||
func (l *ZapLogger) WithFields(fields log.Fields) log.Logger {
|
||||
return NewZapLogger(l.log, l.Prefix(), l.Fields().WithFields(fields))
|
||||
}
|
||||
|
||||
func (l *ZapLogger) Fields() log.Fields {
|
||||
return l.fields
|
||||
}
|
||||
|
||||
func (l *ZapLogger) prepareEntry() *zap.SugaredLogger {
|
||||
newlog := l.log.With(zap.String("prefix", l.Prefix()))
|
||||
if l.fields != nil {
|
||||
fields := make([]zapcore.Field, len(l.fields))
|
||||
idx := 0
|
||||
for k, v := range l.fields {
|
||||
s, _ := json.Marshal(v)
|
||||
fields[idx] = zap.String(k, string(s))
|
||||
idx++
|
||||
}
|
||||
newlog = newlog.With(fields...)
|
||||
}
|
||||
return newlog.Sugar()
|
||||
}
|
||||
|
||||
func (l *ZapLogger) SetLevel(level log.Level) {
|
||||
l.level = level
|
||||
}
|
||||
98
utils/rtp_sort.go
Executable file
98
utils/rtp_sort.go
Executable file
@@ -0,0 +1,98 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"errors"
|
||||
|
||||
"github.com/pion/rtp"
|
||||
)
|
||||
|
||||
const MaxRtpDiff = 65000 //相邻两个包之间的最大差值
|
||||
|
||||
type PriorityQueueRtp struct {
|
||||
itemHeap *packets
|
||||
current *rtp.Packet
|
||||
priorityMap map[uint16]bool
|
||||
lastPacket *rtp.Packet
|
||||
}
|
||||
|
||||
func NewPqRtp() *PriorityQueueRtp {
|
||||
return &PriorityQueueRtp{
|
||||
itemHeap: &packets{},
|
||||
priorityMap: make(map[uint16]bool),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PriorityQueueRtp) Len() int {
|
||||
return p.itemHeap.Len()
|
||||
}
|
||||
|
||||
func (p *PriorityQueueRtp) Push(v rtp.Packet) {
|
||||
if p.priorityMap[v.SequenceNumber] {
|
||||
return
|
||||
}
|
||||
newItem := &packet{
|
||||
value: v,
|
||||
priority: v.SequenceNumber,
|
||||
}
|
||||
heap.Push(p.itemHeap, newItem)
|
||||
}
|
||||
|
||||
func (p *PriorityQueueRtp) Pop() (rtp.Packet, error) {
|
||||
if len(*p.itemHeap) == 0 {
|
||||
return rtp.Packet{}, errors.New("empty queue")
|
||||
}
|
||||
|
||||
item := heap.Pop(p.itemHeap).(*packet)
|
||||
return item.value, nil
|
||||
}
|
||||
|
||||
func (p *PriorityQueueRtp) Empty() {
|
||||
old := *p.itemHeap
|
||||
*p.itemHeap = old[:0]
|
||||
}
|
||||
|
||||
type packets []*packet
|
||||
|
||||
type packet struct {
|
||||
value rtp.Packet
|
||||
priority uint16
|
||||
index int
|
||||
}
|
||||
|
||||
func (p *packets) Len() int {
|
||||
return len(*p)
|
||||
}
|
||||
|
||||
func (p *packets) Less(i, j int) bool {
|
||||
a, b := (*p)[i].priority, (*p)[j].priority
|
||||
if int(a)-int(b) > MaxRtpDiff || int(b)-int(a) > MaxRtpDiff {
|
||||
if a < b {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return a < b
|
||||
}
|
||||
|
||||
func (p *packets) Swap(i, j int) {
|
||||
(*p)[i], (*p)[j] = (*p)[j], (*p)[i]
|
||||
(*p)[i].index = i
|
||||
(*p)[j].index = j
|
||||
}
|
||||
|
||||
func (p *packets) Push(x interface{}) {
|
||||
it := x.(*packet)
|
||||
it.index = len(*p)
|
||||
*p = append(*p, it)
|
||||
}
|
||||
|
||||
func (p *packets) Pop() interface{} {
|
||||
old := *p
|
||||
n := len(old)
|
||||
item := old[n-1]
|
||||
old[n-1] = nil // avoid memory leak
|
||||
item.index = -1 // for safety
|
||||
*p = old[0 : n-1]
|
||||
return item
|
||||
}
|
||||
61
utils/sip.go
61
utils/sip.go
@@ -1,8 +1,10 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
"runtime"
|
||||
"time"
|
||||
)
|
||||
@@ -45,8 +47,67 @@ func randStringBySoure(src string, n int) string {
|
||||
return string(output)
|
||||
}
|
||||
|
||||
// Error Error
|
||||
type Error struct {
|
||||
err error
|
||||
params []interface{}
|
||||
}
|
||||
|
||||
func (err *Error) Error() string {
|
||||
if err == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
str := fmt.Sprint(err.params...)
|
||||
if err.err != nil {
|
||||
str += fmt.Sprintf(" err:%s", err.err.Error())
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
// NewError NewError
|
||||
func NewError(err error, params ...interface{}) error {
|
||||
return &Error{err, params}
|
||||
}
|
||||
func PrintStack() {
|
||||
var buf [4096]byte
|
||||
n := runtime.Stack(buf[:], false)
|
||||
fmt.Printf("==> %s\n", string(buf[:n]))
|
||||
}
|
||||
|
||||
// ResolveSelfIP ResolveSelfIP
|
||||
func ResolveSelfIP() (net.IP, error) {
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, iface := range ifaces {
|
||||
if iface.Flags&net.FlagUp == 0 {
|
||||
continue // interface down
|
||||
}
|
||||
if iface.Flags&net.FlagLoopback != 0 {
|
||||
continue // loopback interface
|
||||
}
|
||||
addrs, err := iface.Addrs()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
if ip == nil || ip.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
continue // not an ipv4 address
|
||||
}
|
||||
return ip, nil
|
||||
}
|
||||
}
|
||||
return nil, errors.New("server not connected to any network")
|
||||
}
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
package utils
|
||||
|
||||
import "encoding/json"
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"golang.org/x/net/html/charset"
|
||||
"golang.org/x/text/encoding/simplifiedchinese"
|
||||
"golang.org/x/text/transform"
|
||||
"io/ioutil"
|
||||
)
|
||||
|
||||
func ToJSONString(v interface{}) string {
|
||||
b, _ := json.Marshal(v)
|
||||
@@ -11,3 +19,23 @@ func ToPrettyString(v interface{}) string {
|
||||
b, _ := json.MarshalIndent(v, "", " ")
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func GbkToUtf8(s []byte) ([]byte, error) {
|
||||
reader := transform.NewReader(bytes.NewReader(s), simplifiedchinese.GBK.NewDecoder())
|
||||
d, e := ioutil.ReadAll(reader)
|
||||
if e != nil {
|
||||
return s, e
|
||||
}
|
||||
return d, nil
|
||||
}
|
||||
|
||||
func DecodeGbk(v interface{}, body []byte) error {
|
||||
bodyBytes, err := GbkToUtf8(body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
decoder := xml.NewDecoder(bytes.NewReader(bodyBytes))
|
||||
decoder.CharsetReader = charset.NewReaderLabel
|
||||
err = decoder.Decode(v)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user