├── .github ├── FUNDING.yml └── workflows │ └── go.yml ├── go.mod ├── _example ├── gopher1.jpg ├── gopher2.jpg ├── gopher3.jpg ├── gopher4.jpg ├── gopher5.jpg ├── gopher6.jpg ├── flying-gopher.avi └── example.go ├── README.md ├── LICENSE └── mjpeg.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: icza 2 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/icza/mjpeg 2 | 3 | go 1.20 4 | -------------------------------------------------------------------------------- /_example/gopher1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/gopher1.jpg -------------------------------------------------------------------------------- /_example/gopher2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/gopher2.jpg -------------------------------------------------------------------------------- /_example/gopher3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/gopher3.jpg -------------------------------------------------------------------------------- /_example/gopher4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/gopher4.jpg -------------------------------------------------------------------------------- /_example/gopher5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/gopher5.jpg -------------------------------------------------------------------------------- /_example/gopher6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/gopher6.jpg -------------------------------------------------------------------------------- /_example/flying-gopher.avi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/icza/mjpeg/HEAD/_example/flying-gopher.avi -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | 14 | build: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v3 18 | 19 | - name: Set up Go 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: 1.19 23 | 24 | - name: Build 25 | run: go build -v ./... 26 | 27 | - name: Test 28 | run: go test -v ./... 29 | -------------------------------------------------------------------------------- /_example/example.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This example encodes all jpeg images (*.jpg) into an MJPEG movie file 4 | (flying-gopher.avi). Images are added in alphabetical order, using FPS=2/sec. 5 | 6 | */ 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "io/ioutil" 12 | "path/filepath" 13 | "sort" 14 | 15 | "github.com/icza/mjpeg" 16 | ) 17 | 18 | func main() { 19 | checkErr := func(err error) { 20 | if err != nil { 21 | panic(err) 22 | } 23 | } 24 | 25 | outName := "flying-gopher.avi" 26 | aw, err := mjpeg.New(outName, 144, 108, 2) 27 | checkErr(err) 28 | 29 | // Create a movie from images: 30 | matches, err := filepath.Glob("*.jpg") 31 | checkErr(err) 32 | sort.Strings(matches) 33 | 34 | fmt.Println("Found images:", matches) 35 | for _, name := range matches { 36 | data, err := ioutil.ReadFile(name) 37 | checkErr(err) 38 | checkErr(aw.AddFrame(data)) 39 | } 40 | 41 | checkErr(aw.Close()) 42 | fmt.Printf("%s was written successfully.\n", outName) 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mjpeg 2 | 3 | ![Build Status](https://github.com/icza/mjpeg/actions/workflows/go.yml/badge.svg) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/icza/mjpeg.svg)](https://pkg.go.dev/github.com/icza/mjpeg) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/icza/mjpeg)](https://goreportcard.com/report/github.com/icza/mjpeg) 6 | 7 | MJPEG video writer implementation in Go. 8 | 9 | ## Examples 10 | 11 | Let's see an example how to turn the JPEG files `1.jpg`, `2.jpg`, ..., `10.jpg` into a movie file: 12 | 13 | checkErr := func(err error) { 14 | if err != nil { 15 | panic(err) 16 | } 17 | } 18 | 19 | // Video size: 200x100 pixels, FPS: 2 20 | aw, err := mjpeg.New("test.avi", 200, 100, 2) 21 | checkErr(err) 22 | 23 | // Create a movie from images: 1.jpg, 2.jpg, ..., 10.jpg 24 | for i := 1; i <= 10; i++ { 25 | data, err := ioutil.ReadFile(fmt.Sprintf("%d.jpg", i)) 26 | checkErr(err) 27 | checkErr(aw.AddFrame(data)) 28 | } 29 | 30 | checkErr(aw.Close()) 31 | 32 | Example to add an `image.Image` as a frame to the video: 33 | 34 | aw, err := mjpeg.New("test.avi", 200, 100, 2) 35 | checkErr(err) 36 | 37 | var img image.Image 38 | // Acquire / initialize image, e.g.: 39 | // img = image.NewRGBA(image.Rect(0, 0, 200, 100)) 40 | 41 | buf := &bytes.Buffer{} 42 | checkErr(jpeg.Encode(buf, img, nil)) 43 | checkErr(aw.AddFrame(buf.Bytes())) 44 | 45 | checkErr(aw.Close()) 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2016 Andras Belicza 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /mjpeg.go: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Package mjpeg contains an MJPEG video format writer. 4 | 5 | Examples 6 | 7 | Let's see an example how to turn the JPEG files 1.jpg, 2.jpg, ..., 10.jpg into a movie file: 8 | 9 | checkErr := func(err error) { 10 | if err != nil { 11 | panic(err) 12 | } 13 | } 14 | 15 | // Video size: 200x100 pixels, FPS: 2 16 | aw, err := mjpeg.New("test.avi", 200, 100, 2) 17 | checkErr(err) 18 | 19 | // Create a movie from images: 1.jpg, 2.jpg, ..., 10.jpg 20 | for i := 1; i <= 10; i++ { 21 | data, err := ioutil.ReadFile(fmt.Sprintf("%d.jpg", i)) 22 | checkErr(err) 23 | checkErr(aw.AddFrame(data)) 24 | } 25 | 26 | checkErr(aw.Close()) 27 | 28 | Example to add an image.Image as a frame to the video: 29 | 30 | aw, err := mjpeg.New("test.avi", 200, 100, 2) 31 | checkErr(err) 32 | 33 | var img image.Image 34 | // Acquire / initialize image, e.g.: 35 | // img = image.NewRGBA(image.Rect(0, 0, 200, 100)) 36 | 37 | buf := &bytes.Buffer{} 38 | checkErr(jpeg.Encode(buf, img, nil)) 39 | checkErr(aw.AddFrame(buf.Bytes())) 40 | 41 | checkErr(aw.Close()) 42 | */ 43 | package mjpeg 44 | 45 | import ( 46 | "encoding/binary" 47 | "errors" 48 | "io" 49 | "log" 50 | "os" 51 | "time" 52 | ) 53 | 54 | var ( 55 | // ErrTooLarge reports if more frames cannot be added, 56 | // else the video file would get corrupted. 57 | ErrTooLarge = errors.New("Video file too large") 58 | 59 | // errImproperUse signals improper state (due to a previous error). 60 | errImproperState = errors.New("Improper State") 61 | ) 62 | 63 | // AviWriter is an *.avi video writer. 64 | // The video codec is MJPEG. 65 | type AviWriter interface { 66 | // AddFrame adds a frame from a JPEG encoded data slice. 67 | AddFrame(jpegData []byte) error 68 | 69 | // Close finalizes and closes the avi file. 70 | Close() error 71 | } 72 | 73 | // aviWriter is the AviWriter implementation. 74 | type aviWriter struct { 75 | // aviFile is the name of the file to write the result to 76 | aviFile string 77 | // width is the width of the video 78 | width int32 79 | // height is the height of the video 80 | height int32 81 | // fps is the frames/second (the "speed") of the video 82 | fps int32 83 | 84 | // avif is the avi file descriptor 85 | avif *os.File 86 | // idxFile is the name of the index file 87 | idxFile string 88 | // idxf is the index file descriptor 89 | idxf *os.File 90 | 91 | // writeErr holds the last encountered write error (to avif) 92 | err error 93 | 94 | // lengthFields contains the file positions of the length fields 95 | // that are filled later; used as a stack (LIFO) 96 | lengthFields []int64 97 | 98 | // Position of the frames count fields 99 | framesCountFieldPos, framesCountFieldPos2 int64 100 | // Position of the MOVI chunk 101 | moviPos int64 102 | 103 | // frames is the number of frames written to the AVI file 104 | frames int 105 | 106 | // General buffers used to write int values. 107 | buf4, buf2 []byte 108 | } 109 | 110 | // New returns a new AviWriter. 111 | // The Close() method of the AviWriter must be called to finalize the video file. 112 | func New(aviFile string, width, height, fps int32) (awr AviWriter, err error) { 113 | aw := &aviWriter{ 114 | aviFile: aviFile, 115 | width: width, 116 | height: height, 117 | fps: fps, 118 | idxFile: aviFile + ".idx_", 119 | lengthFields: make([]int64, 0, 5), 120 | buf4: make([]byte, 4), 121 | buf2: make([]byte, 2), 122 | } 123 | 124 | defer func() { 125 | if err == nil { 126 | return 127 | } 128 | logErr := func(e error) { 129 | if e != nil { 130 | log.Printf("Error: %v\n", e) 131 | } 132 | } 133 | if aw.avif != nil { 134 | logErr(aw.avif.Close()) 135 | logErr(os.Remove(aviFile)) 136 | } 137 | if aw.idxf != nil { 138 | logErr(aw.idxf.Close()) 139 | logErr(os.Remove(aw.idxFile)) 140 | } 141 | }() 142 | 143 | aw.avif, err = os.Create(aviFile) 144 | if err != nil { 145 | return nil, err 146 | } 147 | aw.idxf, err = os.Create(aw.idxFile) 148 | if err != nil { 149 | return nil, err 150 | } 151 | 152 | wstr, wint32, wint16, wLenF, finalizeLenF := 153 | aw.writeStr, aw.writeInt32, aw.writeInt16, aw.writeLengthField, aw.finalizeLengthField 154 | 155 | // Write AVI header 156 | wstr("RIFF") // RIFF type 157 | wLenF() // File length (remaining bytes after this field) (nesting level 0) 158 | wstr("AVI ") // AVI signature 159 | wstr("LIST") // LIST chunk: data encoding 160 | wLenF() // Chunk length (nesting level 1) 161 | wstr("hdrl") // LIST chunk type 162 | wstr("avih") // avih sub-chunk 163 | wint32(0x38) // Sub-chunk length excluding the first 8 bytes of avih signature and size 164 | wint32(1000000 / fps) // Frame delay time in microsec 165 | wint32(0) // dwMaxBytesPerSec (maximum data rate of the file in bytes per second) 166 | wint32(0) // Reserved 167 | wint32(0x10) // dwFlags, 0x10 bit: AVIF_HASINDEX (the AVI file has an index chunk at the end of the file - for good performance); Windows Media Player can't even play it if index is missing! 168 | aw.framesCountFieldPos = aw.currentPos() 169 | wint32(0) // Number of frames 170 | wint32(0) // Initial frame for non-interleaved files; non interleaved files should set this to 0 171 | wint32(1) // Number of streams in the video; here 1 video, no audio 172 | wint32(0) // dwSuggestedBufferSize 173 | wint32(width) // Image width in pixels 174 | wint32(height) // Image height in pixels 175 | wint32(0) // Reserved 176 | wint32(0) 177 | wint32(0) 178 | wint32(0) 179 | 180 | // Write stream information 181 | wstr("LIST") // LIST chunk: stream headers 182 | wLenF() // Chunk size (nesting level 2) 183 | wstr("strl") // LIST chunk type: stream list 184 | wstr("strh") // Stream header 185 | wint32(56) // Length of the strh sub-chunk 186 | wstr("vids") // fccType - type of data stream - here 'vids' for video stream 187 | wstr("MJPG") // MJPG for Motion JPEG 188 | wint32(0) // dwFlags 189 | wint32(0) // wPriority, wLanguage 190 | wint32(0) // dwInitialFrames 191 | wint32(1) // dwScale 192 | wint32(fps) // dwRate, Frame rate for video streams (the actual FPS is calculated by dividing this by dwScale) 193 | wint32(0) // usually zero 194 | aw.framesCountFieldPos2 = aw.currentPos() 195 | wint32(0) // dwLength, playing time of AVI file as defined by scale and rate (set equal to the number of frames) 196 | wint32(0) // dwSuggestedBufferSize for reading the stream (typically, this contains a value corresponding to the largest chunk in a stream) 197 | wint32(-1) // dwQuality, encoding quality given by an integer between (0 and 10,000. If set to -1, drivers use the default quality value) 198 | wint32(0) // dwSampleSize, 0 means that each frame is in its own chunk 199 | wint16(0) // left of rcFrame if stream has a different size than dwWidth*dwHeight(unused) 200 | wint16(0) // ..top 201 | wint16(0) // ..right 202 | wint16(0) // ..bottom 203 | // end of 'strh' chunk, stream format follows 204 | wstr("strf") // stream format chunk 205 | wLenF() // Chunk size (nesting level 3) 206 | wint32(40) // biSize, write header size of BITMAPINFO header structure; applications should use this size to determine which BITMAPINFO header structure is being used, this size includes this biSize field 207 | wint32(width) // biWidth, width in pixels 208 | wint32(height) // biWidth, height in pixels (may be negative for uncompressed video to indicate vertical flip) 209 | wint16(1) // biPlanes, number of color planes in which the data is stored 210 | wint16(24) // biBitCount, number of bits per pixel # 211 | wstr("MJPG") // biCompression, type of compression used (uncompressed: NO_COMPRESSION=0) 212 | wint32(width * height * 3) // biSizeImage (buffer size for decompressed mage) may be 0 for uncompressed data 213 | wint32(0) // biXPelsPerMeter, horizontal resolution in pixels per meter 214 | wint32(0) // biYPelsPerMeter, vertical resolution in pixels per meter 215 | wint32(0) // biClrUsed (color table size; for 8-bit only) 216 | wint32(0) // biClrImportant, specifies that the first x colors of the color table (0: all the colors are important, or, rather, their relative importance has not been computed) 217 | finalizeLenF() //'strf' chunk finished (nesting level 3) 218 | 219 | wstr("strn") // Use 'strn' to provide a zero terminated text string describing the stream 220 | name := "Created with https://github.com/icza/mjpeg" + 221 | " at " + time.Now().Format("2006-01-02 15:04:05 MST") 222 | // Name must be 0-terminated and stream name length (the length of the chunk) must be even 223 | if len(name)&0x01 == 0 { 224 | name = name + " \000" // padding space plus terminating 0 225 | } else { 226 | name = name + "\000" // terminating 0 227 | } 228 | wint32(int32(len(name))) // Length of the strn sub-CHUNK (must be even) 229 | wstr(name) 230 | finalizeLenF() // LIST 'strl' finished (nesting level 2) 231 | finalizeLenF() // LIST 'hdrl' finished (nesting level 1) 232 | 233 | wstr("LIST") // The second LIST chunk, which contains the actual data 234 | wLenF() // Chunk length (nesting level 1) 235 | aw.moviPos = aw.currentPos() 236 | wstr("movi") // LIST chunk type: 'movi' 237 | 238 | if aw.err != nil { 239 | return nil, aw.err 240 | } 241 | 242 | return aw, nil 243 | } 244 | 245 | // writeStr writes a string to the file. 246 | func (aw *aviWriter) writeStr(s string) { 247 | if aw.err != nil { 248 | return 249 | } 250 | _, aw.err = aw.avif.WriteString(s) 251 | } 252 | 253 | // writeInt32 writes a 32-bit int value to the file. 254 | func (aw *aviWriter) writeInt32(n int32) { 255 | if aw.err != nil { 256 | return 257 | } 258 | binary.LittleEndian.PutUint32(aw.buf4, uint32(n)) 259 | _, aw.err = aw.avif.Write(aw.buf4) 260 | } 261 | 262 | // writeIdxInt32 writes a 32-bit int value to the index file. 263 | func (aw *aviWriter) writeIdxInt32(n int32) { 264 | if aw.err != nil { 265 | return 266 | } 267 | binary.LittleEndian.PutUint32(aw.buf4, uint32(n)) 268 | _, aw.err = aw.idxf.Write(aw.buf4) 269 | } 270 | 271 | // writeInt16 writes a 16-bit int value to the index file. 272 | func (aw *aviWriter) writeInt16(n int16) { 273 | if aw.err != nil { 274 | return 275 | } 276 | binary.LittleEndian.PutUint16(aw.buf2, uint16(n)) 277 | _, aw.err = aw.avif.Write(aw.buf2) 278 | } 279 | 280 | // writeLengthField writes an empty int field to the avi file, and saves 281 | // the current file position as it will be filled later. 282 | func (aw *aviWriter) writeLengthField() { 283 | if aw.err != nil { 284 | return 285 | } 286 | pos := aw.currentPos() 287 | aw.lengthFields = append(aw.lengthFields, pos) 288 | aw.writeInt32(0) 289 | } 290 | 291 | // finalizeLengthField finalizes the last length field. 292 | func (aw *aviWriter) finalizeLengthField() { 293 | if aw.err != nil { 294 | return 295 | } 296 | pos := aw.currentPos() 297 | if aw.err != nil { 298 | return 299 | } 300 | 301 | numLenFs := len(aw.lengthFields) 302 | if numLenFs == 0 { 303 | aw.err = errImproperState 304 | return 305 | } 306 | aw.seek(aw.lengthFields[numLenFs-1], 0) 307 | aw.lengthFields = aw.lengthFields[:numLenFs-1] 308 | aw.writeInt32(int32(pos - aw.currentPos() - 4)) 309 | 310 | // Seek "back" but align to a 2-byte boundary 311 | if pos&0x01 != 0 { 312 | pos++ 313 | } 314 | aw.seek(pos, 0) 315 | } 316 | 317 | // seek seeks the AVI file. 318 | func (aw *aviWriter) seek(offset int64, whence int) (pos int64) { 319 | if aw.err != nil { 320 | return 321 | } 322 | pos, aw.err = aw.avif.Seek(offset, whence) 323 | return 324 | } 325 | 326 | // currentPos returns the current file position of the AVI file. 327 | func (aw *aviWriter) currentPos() int64 { 328 | return aw.seek(0, 1) // Seek relative to current pos 329 | } 330 | 331 | // AddFrame implements AviWriter.AddFrame(). 332 | // ErrTooLarge is returned if the vide file is too large and would get corrupted 333 | // if the given image would be added. The file limit is about 4GB. 334 | func (aw *aviWriter) AddFrame(jpegData []byte) error { 335 | framePos := aw.currentPos() 336 | // Pointers in AVI are 32 bit. Do not write beyond that else the whole AVI file will be corrupted (not playable). 337 | // Index entry size: 16 bytes (for each frame) 338 | if framePos+int64(len(jpegData))+int64(aw.frames*16) > 4200000000 { // 2^32 = 4 294 967 296 339 | return ErrTooLarge 340 | } 341 | 342 | aw.frames++ 343 | 344 | aw.writeInt32(0x63643030) // "00dc" compressed frame 345 | aw.writeLengthField() // Chunk length (nesting level 2) 346 | if aw.err == nil { 347 | _, aw.err = aw.avif.Write(jpegData) 348 | } 349 | aw.finalizeLengthField() // "00dc" chunk finished (nesting level 2) 350 | 351 | // Write index data 352 | aw.writeIdxInt32(0x63643030) // "00dc" compressed frame 353 | aw.writeIdxInt32(0x10) // flags: select AVIIF_KEYFRAME (The flag indicates key frames in the video sequence. Key frames do not need previous video information to be decompressed.) 354 | aw.writeIdxInt32(int32(framePos - aw.moviPos)) // offset to the chunk, offset can be relative to file start or 'movi' 355 | aw.writeIdxInt32(int32(len(jpegData))) // length of the chunk 356 | 357 | return aw.err 358 | } 359 | 360 | // Close implements AviWriter.Close(). 361 | func (aw *aviWriter) Close() (err error) { 362 | defer func() { 363 | aw.avif.Close() 364 | aw.idxf.Close() 365 | os.Remove(aw.idxFile) 366 | }() 367 | 368 | aw.finalizeLengthField() // LIST 'movi' finished (nesting level 1) 369 | 370 | // Write index 371 | aw.writeStr("idx1") // idx1 chunk 372 | var idxLength int64 373 | if aw.err == nil { 374 | idxLength, aw.err = aw.idxf.Seek(0, 1) // Seek relative to current pos 375 | } 376 | aw.writeInt32(int32(idxLength)) // Chunk length (we know its size, no need to use writeLengthField() and finalizeLengthField() pair) 377 | // Copy temporary index data 378 | if aw.err == nil { 379 | _, aw.err = aw.idxf.Seek(0, 0) 380 | } 381 | if aw.err == nil { 382 | _, aw.err = io.Copy(aw.avif, aw.idxf) 383 | } 384 | 385 | pos := aw.currentPos() 386 | aw.seek(aw.framesCountFieldPos, 0) 387 | aw.writeInt32(int32(aw.frames)) 388 | aw.seek(aw.framesCountFieldPos2, 0) 389 | aw.writeInt32(int32(aw.frames)) 390 | aw.seek(pos, 0) 391 | 392 | aw.finalizeLengthField() // 'RIFF' File finished (nesting level 0) 393 | 394 | return aw.err 395 | } 396 | --------------------------------------------------------------------------------