POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Have some questions or having issues with your IP Camera(s), Post them here for the mods and other users to assist you with.
Post Reply
tickerguy
Posts: 19
Joined: Thu Nov 30, 2017 1:12 pm

POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by tickerguy »

Both models behave identically.

The API (Amcrest-HTTP_API_V3.26.pdf) says this should work and it does to my Amcrest doorbell camera.

However, if I try to send an audio capture to one of the above (both are on current firmware) it hangs instead of returning the authentication request and the nasty part is that the hang appears to be permanent (the socket is open but no data.)

Example:

Code: Select all

root@TnHouse:/usr/local/etc/HD-MCP curl -X POST 'http://192.168.10.22/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1' -H 'Content-Type: Audio/AMR' --data-binary '@/tmp/HD-MCP/LivingRm_Camera.amr' -v -u 'admin:password$' --digest
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 192.168.10.22:80...
* Connected to 192.168.10.22 (192.168.10.22) port 80
* Server auth using Digest with user 'admin'
> POST /cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1 HTTP/1.1
> Host: 192.168.10.22
> User-Agent: curl/8.8.0
> Accept: */*
> Content-Type: Audio/AMR
> Content-Length: 0
>
* Request completely sent off
^C

I should get back a "401 Unauthorized" with a "WWW-Authenticate:" header containing the realm, nonce and similar -- but I do not, and thus the curl command hangs forever.

If I send the same file to my doorbell (Amcrest model) it works (no, that's not the real password) :-)

Code: Select all

root@TnHouse:/usr/local/etc/HD-MCP # curl -X POST 'http://192.168.4.126/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1' -H 'Content-Type: Audio/AMR' --data-binary '@/tmp/HD-MCP/LivingRm_Camera.amr' -v -u 'admin:password' --digest
Note: Unnecessary use of -X or --request, POST is already inferred.
*   Trying 192.168.4.126:80...
* Connected to 192.168.4.126 (192.168.4.126) port 80
* Server auth using Digest with user 'admin'
> POST /cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1 HTTP/1.1
> Host: 192.168.4.126
> User-Agent: curl/8.8.0
> Accept: */*
> Content-Type: Audio/AMR
> Content-Length: 0
>
* Request completely sent off
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Digest realm="Login to Z1738F183FEF6", qop="auth", nonce="672666334", opaque="37a266f79d50f73e70e91f40b038d5294157eea9"
< Connection: close
< CONTENT-LENGTH: 0
<
* Closing connection
* Issue another request to this URL: 'http://192.168.4.126/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1'
* Hostname 192.168.4.126 was found in DNS cache
*   Trying 192.168.4.126:80...
* Connected to 192.168.4.126 (192.168.4.126) port 80
* Server auth using Digest with user 'admin'
> POST /cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1 HTTP/1.1
> Host: 192.168.4.126
> Authorization: Digest username="admin", realm="Login to Z1738F183FEF6", nonce="672666334", uri="/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1", cnonce="Y2M5MDZjOWM1OTEyODY0NDU4NDY5MTY3MThiYWU5MWI=", nc=00000001, qop=auth, response="d311442f6ec1eadd11127fabc9800d20", opaque="37a266f79d50f73e70e91f40b038d5294157eea9"
> User-Agent: curl/8.8.0
> Accept: */*
> Content-Type: Audio/AMR
> Content-Length: 3014
>
* upload completely sent off: 3014 bytes
< HTTP/1.1 200 OK
< X-XSS-Protection: 1;mode=block
< X-Frame-Options: SAMEORIGIN
< Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'
< Strict-Transport-Security: max-age=604800; includeSubDomains
< CONNECTION: Keep-Alive
< CONTENT-LENGTH: 2
<
* Connection #1 to host 192.168.4.126 left intact
OKroot@TnHouse:/usr/local/etc/HD-MCP #
Querying things (anything using GET) works as expected which is a problem because the camera CLAIMS it has a speaker channel available, so I can't check for this sort of problem ahead of time and thus there's no way to know (other than "whitelisting" models or something similar) in my application code -- and trying it would be ridiculously ill-advised since I'd either have to implement a timeout on the socket entirely or risk hanging the socket which runs the risk of resource exhaustion since the user might try several times in quick succession when they hear nothing.

Everything else as expected (video/audio playing from the camera, ptz, etc.) works as the API documents, but this does not for everything I have except the doorbell.

I've sent in a support request and don't see anything on the board here about this with a quick search, so I thought I'd ask here and see if anyone has ideas as to what's going on.

Thanks in advance.
tickerguy
Posts: 19
Joined: Thu Nov 30, 2017 1:12 pm

Re: POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by tickerguy »

I have figured out how to resolve this by sniffing packets off TinyCam Pro on my Android (which can access the speaker.) For those who may run into this in the future here's the down-low on it.

Despite Amcrest removing "BASIC" authentication in general (a good thing) many years ago for access to the cameras you need to use it in THIS INSTANCE ONLY. These older cameras will NOT reply to a digest authentication being missing; it just sits (the DOORBELL does work with digest!) -- but "basic" works FOR THIS FUNCTION. In addition despite the claims of the API that these cameras understand and can process several different audio formats that, at least for these older units is BS -- you must encode the audio as G.711A or you will get garbage/static from the speaker, which for most use cases means you need to transcode whatever your source is because that's not exactly a common capture format. That is, it appears the cameras IGNORE the Content-Type: header in that they do not reject a request with any of the other allegedly-permitted formats and simply presume anything coming in is G.711A and try to emit it out the speaker.

The transmission will stay open even with a content length (the camera appears to ignore it even if you send a non-zero length) to permit "long form" or "continuous" transmission to the speaker, so you are responsible for closing it when done.

This pipeline command pair from an AMR file works; obviously programmatically you want to do it a bit differently, but this is the workflow you need to generate for it to work:

Code: Select all

ffmpeg -i file.amr -f alaw pipe: | curl --data-binary '@-' -H 'Content-Type: Audio/G.711A' 'http://192.168.4.212/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1' -u 'login:password' --max-time 5 -v
That takes an AMR file, transcodes it to G.711A alaw and stuffs it in curl using BASIC authentication, exiting after 5 seconds. Provided the file is of reasonable size (e.g. 10s of seconds of speech) the camera will buffer it and also gives you feedback as to what its doing (the "-v" switch.) If the audio will not fit in the camera's buffer (e.g. the camera may flow control the incoming stream, such as if it was a file full of music) then you need to get more-creative programmatically so as to keep the connection open until the entire contents you are feeding it is in the camera.
tickerguy
Posts: 19
Joined: Thu Nov 30, 2017 1:12 pm

Re: POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by tickerguy »

BTW ignoring the "Content-Length" header is quite evil (in that it can produce client HANGS) and a rank violation of the HTTP protocol. If you want to send unlimited streaming to the speaker then sending a "0" length and expecting the client to just disconnect when its done is fine, but if you tell the other end you're going to POST 13,023 bytes to it of content (after the headers) the HTTP specification REQUIRES the receiving server to return a acceptance or rejection status code to you once you have.

These cameras do NOT do that so coding to use this capability requires YOU implement a timeout (without really knowing if the entire transmission has been "swallowed" successfully or not by the camera) and unceremoniously dropping the socket from YOUR end without any way to know if the camera has in fact emitted (or will emit) all the sound you sent. I have not been able to find any way around this and it was quite a puzzler as to why I was never getting back the status code expected even though I had sent the number of bytes of content I declared I intended to in the Content-Length: header.
User avatar
Volkemon
Posts: 41
Joined: Mon Dec 30, 2024 10:32 am

Re: POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by Volkemon »

WAY above my current 'pay grade' but a great read none the less. Thanks for sharing all the details.
tickerguy
Posts: 19
Joined: Thu Nov 30, 2017 1:12 pm

Re: POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by tickerguy »

Here's some more on it -- the forum didn't let me add the sample code as you posted in the middle :-)

The hang when a content length is declared is serious and IMHO merits a fix and code release for all impacted models as if you do not implement a timeout in your own code it will hang forever.

Here is a piece of test code that illustrates this problem and should compile on most Unix boxes (I run FreeBSD) that have ffmpeg and curl installed. It will abort on the timeout (set as 2 seconds but if you're using a longer test file you might want to set it to a larger value) and print the error message before the "Complete!" line when it should not hang for the timeout period.

Compile with:
$ cc -I/usr/local/include -L/usr/local/lib -g -o test-curl test-curl.c -lcurl

Code: Select all

/*
 * A quick-and-dirty (please, no comments on the lack of bounds checking
 * and such) piece of code to demonstrate the use of the curl library to
 * emit sound to an Amcreat camera.
 *
 * You must fill in the correct authentication details (the IP address and
 * login/password combination) in the below code before compiling, and then
 * provide one argument which is a file in an audio format that "ffmpeg"
 * can understand (an "AMR" file is recommended but anything ffmpeg can
 * determine will work.)  ffmpeg is presumed to be on your system in
 * /usr/local/bin.
 *
 * Note that debugging and stderr are enabled so you can see what's going on
 * in detail.
 *
 * Although the "Content-Length:" header IS provided and is correct
 * with the total of the transcoded data to be sent THE CAMERA DOES NOT
 * RETURN STATUS FROM THE POST and thus the timeout fires.
 *
 * THIS IS A SERIOUS VIOLATION of the HTTP specifications in that if you do
 * not set up to handle breaking out of the wait for said response code
 * yourself (e.g. with a timeout as I set here) your code will hang FOREVER as the socket
 * is open and valid, but no data will ever come back.
 */


#include        <stdio.h>
#include        <sys/types.h>
#include        <fcntl.h>
#include        <unistd.h>
#include        <stdlib.h>


#include        </usr/local/include/curl/curl.h>

int     main(int argc, char *argv[])

{

        CURL    *easy_handle;
        CURLcode result;
        struct  curl_slist *h_list = NULL;

        int     id;

        FILE    *p;

        char    tmp[512];
        unsigned char   buffer[512000];
        unsigned char   databuf[512000];
        int     size;

        char    *args[20];
        int     w_fds[2];
        int     r_fds[2];
        int     pid;

        int     tcsize = 0;
        int     x;
        int     stop = 0;

        args[0] = "/usr/local/bin/ffmpeg";
        args[1] = "-i";
        args[2] = "pipe:";
        args[3] = "-f";
        args[4] = "alaw";
        args[5] = "pipe:";
//      args[6] = "-loglevel";
//      args[7] = "quiet";
        args[6] = NULL;

        pipe(w_fds);
        pipe(r_fds);

        pid = fork();
        switch(pid) {
                case -1:
                        fprintf(stderr, "Failed to fork");
                        exit(1);
                        break;
                case 0: /* Child */
                        close(0);
                        close(1);
//                      close(2);
                        dup2(w_fds[0], 0);
                        dup2(r_fds[1], 1);      /* Assign STDIO */
//                      open("/dev/null", O_RDWR);      /* Stderr */
                        closefrom(3);
                        execvp(args[0], args);
                        fprintf(stderr, "Failed to exec");
                        exit(1);
                        break;
                default:
                        close(w_fds[0]);
                        close(r_fds[1]);
                        break;
        }

        id = open(argv[1], O_RDONLY);
        if (id == -1) {
                perror("Error opening speech file");
                exit(1);
        }
        size = read(id, buffer, sizeof(buffer));        /* Read the file to be sent in */
        close(id);

        write(w_fds[1], buffer, size);
        close(w_fds[1]);
        while (!stop) {
                x = read(r_fds[0], &databuf[tcsize], sizeof(databuf) - tcsize);
                if (x) {
                        tcsize += x;
                } else {
                        stop++;
                }
        }
        close(r_fds[0]);

        curl_global_init(CURL_GLOBAL_ALL);
        easy_handle = curl_easy_init(); /* Get an init */

        if (easy_handle == NULL) {
                perror("Error initializing curl");
                exit(1);
        }

        h_list = curl_slist_append(h_list, "Content-Type: Audio/G.711A");
        sprintf(tmp, "Content-Length: %d", tcsize);
        h_list = curl_slist_append(h_list, tmp);

        curl_easy_setopt(easy_handle, CURLOPT_URL, "http://192.168.x.x/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1");
        curl_easy_setopt(easy_handle, CURLOPT_HTTPAUTH, CURLAUTH_ANY);
        curl_easy_setopt(easy_handle, CURLOPT_USERPWD, "user:password");
        curl_easy_setopt(easy_handle, CURLOPT_HTTPHEADER, h_list);
        curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDSIZE, tcsize);
        curl_easy_setopt(easy_handle, CURLOPT_POSTFIELDS, databuf);
        curl_easy_setopt(easy_handle, CURLOPT_NOSIGNAL, 1);
        curl_easy_setopt(easy_handle, CURLOPT_TIMEOUT, 2);
        curl_easy_setopt(easy_handle, CURLOPT_VERBOSE, 1);


        result = curl_easy_perform(easy_handle);
        if (result != CURLE_OK) {
                fprintf(stderr, "Error: %d ", result);
                perror("Curl error");
        }
        printf("Complete!\n");

        curl_easy_cleanup(easy_handle);

        curl_global_cleanup();
        exit(0);

}
tickerguy
Posts: 19
Joined: Thu Nov 30, 2017 1:12 pm

Re: POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by tickerguy »

Further investigation has disclosed that the AD410 doorbell camera has an even more-serious issue that precludes running outbound ("speak") audio to the speaker at all, except through their app which apparently is using a back-channel authenticated RTSP stream I've not been able to intercept successfully as of yet (that is, even if you use the app and you're in the same building its not going directly to the camera) and that is likely unable to be intercepted at all because their AWS-based server (with which its talking) is sending encrypted traffic, so determining exactly what it is sending is not possible (e.g. is it sending a POST, is it using back-channel RTSP -- not sure.)

That is the AD410 refuses BASIC authentication on a POST and demands DIGEST. That's fine except that the way this works generally is that the first request comes back with an "Unauthorized" response and the nonce, qop and such you need to compute the digest. Fine, you do that and send the content but on that second request the camera does indeed accept it and send back an "ok" immediately after swallowing the number of bytes you specified to be transferred (good) but then since you got the reply back your end will close the connection and as soon as you do the speech is terminated.

The result is that all you get is the "click" if the speaker unmuting AND NEVER THE ACTUAL SPEECH.

There may also be a problem with it refusing to honor the codec but I can't be sure about that -- all I get is the "click" and not "random noise" (which is what happens with a codec mismatch.)

Thus while I now have "working" (albeit with a serious bit of stupidity in their code I have to work around) for the subject cameras (and presumably anything else that's an indoor with a speaker) that doesn't work for the doorbell even though the http server on the doorbell says it did, and exactly why is not clear since the doorbell SAYS it is happy with the transmission.

Traceback from said test:

Code: Select all

$ ./test-curl test.amr
ffmpeg version 6.1.1 Copyright (c) 2000-2023 the FFmpeg developers
  built with FreeBSD clang version 16.0.6 (https://github.com/llvm/llvm-project.git llvmorg-16.0.6-0-g7cbf1a259152)
  configuration: --prefix=/usr/local --mandir=/usr/local/share/man --datadir=/usr/local/share/ffmpeg --docdir=/usr/local/share/doc/ffmpeg --pkgconfigdir=/usr/local/libdata/pkgconfig --disable-static --disable-libcelt --enable-shared --enable-pic --enable-gpl --cc=cc --cxx=c++ --disable-alsa --disable-libopencore-amrnb --disable-libopencore-amrwb --enable-libaom --disable-libaribb24 --disable-libaribcaption --enable-asm --enable-libass --disable-libbs2b --disable-libcaca --disable-libcdio --disable-libcodec2 --enable-libdav1d --disable-libdavs2 --disable-libdc1394 --disable-debug --enable-htmlpages --enable-libdrm --disable-libfdk-aac --disable-libflite --enable-fontconfig --enable-libfreetype --enable-frei0r --disable-libfribidi --disable-gcrypt --disable-libglslang --disable-libgme --enable-gmp --enable-gnutls --enable-version3 --disable-libgsm --enable-libharfbuzz --enable-iconv --disable-libilbc --disable-libjack --enable-libjxl --disable-libklvanc --disable-libkvazaar --disable-ladspa --enable-libmp3lame --enable-lcms2 --disable-liblensfun --disable-libbluray --enable-libplacebo --disable-librsvg --enable-libxml2 --disable-lv2 --disable-mbedtls --disable-libmodplug --disable-libmysofa --enable-network --disable-nonfree --disable-nvenc --disable-openal --disable-opencl --disable-opengl --disable-libopenh264 --disable-libopenjpeg --disable-libopenmpt --disable-openssl --disable-libopenvino --enable-optimizations --enable-libopus --disable-pocketsphinx --disable-libpulse --disable-librabbitmq --disable-librav1e --disable-librist --enable-runtime-cpudetect --disable-librubberband --disable-sdl2 --enable-libshaderc --disable-libsmbclient --disable-libsnappy --disable-sndio --disable-libsoxr --disable-libspeex --disable-libsrt --disable-libssh --enable-libsvtav1 --disable-libtensorflow --disable-libtesseract --disable-libtheora --disable-libtwolame --disable-libuavs3d --enable-libv4l2 --enable-vaapi --disable-vapoursynth --enable-vdpau --disable-libvidstab --enable-libvmaf --enable-libvorbis --disable-libvo-amrwbenc --enable-libvpx --enable-vulkan --enable-libwebp --enable-libx264 --enable-libx265 --disable-libxavs2 --enable-libxcb --disable-libxvid --disable-outdev=xv --disable-libzimg --disable-libzmq --disable-libzvbi
  libavutil      58. 29.100 / 58. 29.100
  libavcodec     60. 31.102 / 60. 31.102
  libavformat    60. 16.100 / 60. 16.100
  libavdevice    60.  3.100 / 60.  3.100
  libavfilter     9. 12.100 /  9. 12.100
  libswscale      7.  5.100 /  7.  5.100
  libswresample   4. 12.100 /  4. 12.100
  libpostproc    57.  3.100 / 57.  3.100
Input #0, amr, from 'pipe:':
  Duration: N/A, bitrate: 12 kb/s
  Stream #0:0: Audio: amr_nb (samr / 0x726D6173), 8000 Hz, mono, fltp, 12 kb/s
Stream mapping:
  Stream #0:0 -> #0:0 (amr_nb (amrnb) -> pcm_alaw (native))
Output #0, alaw, to 'pipe:':
  Metadata:
    encoder         : Lavf60.16.100
  Stream #0:0: Audio: pcm_alaw, 8000 Hz, mono, s16, 64 kb/s
    Metadata:
      encoder         : Lavc60.31.102 pcm_alaw
[out#0/alaw @ 0x391d8ce24180] video:0kB audio:14kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.000000%
size=      14kB time=00:00:01.78 bitrate=  64.7kbits/s speed= 112x
This shows we took an AMR file and turned it into G.711A alaw stream; this same stream plays perfectly well on the subject cameras however as noted above those cameras only take BASIC authentication and hang without returning a status once the upload is complete, so you need the timeout. The AD410 doorbell behaves differently with the same API call:

Code: Select all

*   Trying 192.168.4.126:80...
* Connected to 192.168.4.126 (192.168.4.126) port 80
> POST /cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1 HTTP/1.1
Host: 192.168.4.126
Accept: */*
Content-Type: Audio/G.711A
Content-Length: 14400

* upload completely sent off: 14400 bytes
< HTTP/1.1 401 Unauthorized
< WWW-Authenticate: Digest realm="Login to Z1738F183FEF6", qop="auth", nonce="587431416", opaque="37a266f79d50f73e70e91f40b038d5294157eea9"
< Connection: close
< CONTENT-LENGTH: 0
<
The AD410 says "Gimme DIGEST authentication" as per the standard. Good, so the curl library will do that.

Code: Select all

* Need to rewind upload for next request
* Closing connection
* Issue another request to this URL: 'http://192.168.4.126/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1'
* Hostname 192.168.4.126 was found in DNS cache
*   Trying 192.168.4.126:80...
* Connected to 192.168.4.126 (192.168.4.126) port 80
* Server auth using Digest with user 'admin'
> POST /cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1 HTTP/1.1
Host: 192.168.4.126
Authorization: Digest username="admin", realm="Login to Z1738F183FEF6", nonce="587431416", uri="/cgi-bin/audio.cgi?action=postAudio&httptype=singlepart&channel=1", cnonce="MWMxNTY2OTRlNjM3YWZjNmNhOGUxMDhkNzQ4NzJmYjc=", nc=00000001, qop=auth, response="81cd8b521b7d46fca91b9ddc2266dfff", opaque="37a266f79d50f73e70e91f40b038d5294157eea9"
Accept: */*
Content-Type: Audio/G.711A
Content-Length: 14400

* upload completely sent off: 14400 bytes
< HTTP/1.1 200 OK
< X-XSS-Protection: 1;mode=block
< X-Frame-Options: SAMEORIGIN
< Content-Security-Policy: script-src 'self' 'unsafe-inline' 'unsafe-eval'
< Strict-Transport-Security: max-age=604800; includeSubDomains
< CONNECTION: Keep-Alive
< CONTENT-LENGTH: 2
<
* Connection #1 to host 192.168.4.126 left intact
OKComplete!
$
The doorbell takes the upload and returns a "200 OK" along with a "same origin" protection header and a few others (e.g. STS, if you were using SSL which we're not.) Note that we get back "OK and "Complete" with an immediate exit -- NO TIMEOUT, so the doorbell appears to have properly taken the upload.

However, there is no sound emitted other than the click of the speaker unmuting (being powered on) and then turning back off!

It appears that the doorbell's closing of the connection (which you can't prohibit on the client end since you sent a complete frame with the length and then followed it with the actual data) causes the doorbell to immediately abort what was going to be speech output.

C'mon Amcrest, THAT needs to be fixed!
tickerguy
Posts: 19
Joined: Thu Nov 30, 2017 1:12 pm

Re: POST to speaker (play audio) hangs IP2M-841 and IP3M-941

Post by tickerguy »

Amcrest has replied to my support request and has stated that their doorbell cams are ONLY documented to work through their app and the API doesn't apply to them.

Well, clearly some (in fact nearly all) of the API does (e.g. being able to get an RTSP stream that way along with being able to set and read config parameters which -- other than the exceptions of the hardware not being there appears to ALL work) but if they consider the use of posting audio "out of scope" then there's likely no hope with their doorbell units and I'll need to look elsewhere for that application.
Post Reply