From cbc6fc2b24bdefef57724ddd1151db9e27de110e Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 30 Jun 2024 19:34:29 +0200 Subject: [PATCH 1/2] Updated --- cmd/ffmpeg/mux/generate.go_old | 113 ------------ cmd/ffmpeg/mux/main.go | 129 ------------- cmd/ffmpeg/mux/stream.go | 256 -------------------------- manager.go | 5 +- writer.go | 320 --------------------------------- writer_test.go | 84 --------- 6 files changed, 3 insertions(+), 904 deletions(-) delete mode 100644 cmd/ffmpeg/mux/generate.go_old delete mode 100644 cmd/ffmpeg/mux/main.go delete mode 100644 cmd/ffmpeg/mux/stream.go delete mode 100644 writer.go delete mode 100644 writer_test.go diff --git a/cmd/ffmpeg/mux/generate.go_old b/cmd/ffmpeg/mux/generate.go_old deleted file mode 100644 index 02657a9..0000000 --- a/cmd/ffmpeg/mux/generate.go_old +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - ff "github.com/mutablelogic/go-media/sys/ffmpeg61" -) - -//////////////////////////////////////////////////////////////////////////////// - - -// Prepare a 16 bit dummy audio frame of 'frame_size' samples and 'nb_channels' channels -func get_audio_frame(stream *Stream) *AVFrame { - AVFrame *frame = stream.tmp_frame - - int j, i, v; - int16_t *q = (int16_t*)frame->data[0]; - - /* check if we want to generate more frames */ - if (av_compare_ts(ost->next_pts, ost->enc->time_base, - STREAM_DURATION, (AVRational){ 1, 1 }) > 0) - return NULL; - - for (j = 0; j nb_samples; j++) { - v = (int)(sin(ost->t) * 10000); - for (i = 0; i < ost->enc->ch_layout.nb_channels; i++) - *q++ = v; - ost->t += ost->tincr; - ost->tincr += ost->tincr2; - } - - frame->pts = ost->next_pts; - ost->next_pts += frame->nb_samples; - - return frame; - } - -/* - * encode one audio frame and send it to the muxer - * return true when encoding is finished - */ -func write_audio_frame(ctx *ff.AVFormatContext, stream *Stream) bool { - frame := get_audio_frame(stream) - if frame != nil { - // convert samples from native format to destination codec format, using the resampler - // compute destination number of samples - delay := ff.SWResample_get_delay(stream.SWRContext(), stream.Encoder().SampleRate()) + frame.NumSamples() - dst_nb_samples := ff.AVUtil_rescale_rnd(delay,ctx.SampleRate(),ctx.SampleRate(),ff.AV_ROUND_UP); - - // When we pass a frame to the encoder, it may keep a reference to it internally; make sure we do not overwrite it here - ff.AVUtil_frame_make_writable(stream.Frame()) - - // Convert to destination format - ff.SWResample_convert_frame(stream.swr_ctx,frame,stream.frame) - - return false - } - frame = ost->frame; - - frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base); - ost->samples_count += dst_nb_samples; - } - return write_frame(ctx, stream, frame) -} - -func write_video_frame(ctx *ff.AVFormatContext, stream *Stream) bool { - return true -} - - - -/* - static int write_audio_frame(AVFormatContext *oc, OutputStream *ost) - { - AVCodecContext *c; - AVFrame *frame; - int ret; - int dst_nb_samples; - - c = ost->enc; - - frame = get_audio_frame(ost); - - if (frame) { - /* convert samples from native format to destination codec format, using the resampler - /* compute destination number of samples - dst_nb_samples = av_rescale_rnd(swr_get_delay(ost->swr_ctx, c->sample_rate) + frame->nb_samples, - c->sample_rate, c->sample_rate, AV_ROUND_UP); - av_assert0(dst_nb_samples == frame->nb_samples); - - /* when we pass a frame to the encoder, it may keep a reference to it - * internally; - * make sure we do not overwrite it here - */ - ret = av_frame_make_writable(ost->frame); - if (ret < 0) - exit(1); - - /* convert to destination format - ret = swr_convert(ost->swr_ctx, - ost->frame->data, dst_nb_samples, - (const uint8_t **)frame->data, frame->nb_samples); - if (ret < 0) { - fprintf(stderr, "Error while converting\n"); - exit(1); - } - frame = ost->frame; - - frame->pts = av_rescale_q(ost->samples_count, (AVRational){1, c->sample_rate}, c->time_base); - ost->samples_count += dst_nb_samples; - } - - return write_frame(oc, c, ost->st, frame, ost->tmp_pkt); - } - */ diff --git a/cmd/ffmpeg/mux/main.go b/cmd/ffmpeg/mux/main.go deleted file mode 100644 index 1c10c3b..0000000 --- a/cmd/ffmpeg/mux/main.go +++ /dev/null @@ -1,129 +0,0 @@ -package main - -import ( - "flag" - "fmt" - "log" - - // Packages - ff "github.com/mutablelogic/go-media/sys/ffmpeg61" -) - -const ( - STREAM_DURATION = 10.0 -) - -func main() { - out := flag.String("out", "", "output file") - flag.Parse() - - // Check in and out - if *out == "" { - log.Fatal("-out flag must be specified") - } - - // Allocate the output media context - ctx, err := ff.AVFormat_create_file(*out, nil) - if err != nil { - log.Fatal(err) - } - defer ff.AVFormat_close_writer(ctx) - - // Add the audio and video streams using the default format codecs and initialize the codecs. - var video, audio *Stream - if codec := ctx.Output().VideoCodec(); codec != ff.AV_CODEC_ID_NONE { - if stream, err := NewStream(ctx, codec); err != nil { - log.Fatalf("could not add video stream: %v", err) - } else { - video = stream - } - defer video.Close() - } - if codec := ctx.Output().AudioCodec(); codec != ff.AV_CODEC_ID_NONE { - if stream, err := NewStream(ctx, codec); err != nil { - log.Fatalf("could not add audio stream: %v", err) - } else { - audio = stream - } - defer audio.Close() - } - - // Now that all the parameters are set, we can open the audio - // and video codecs and allocate the necessary encode buffers. - if video != nil { - // TODO: AVDictionary of options - if err := video.Open(nil); err != nil { - log.Fatalf("could not open video codec: %v", err) - } - } - if audio != nil { - // TODO: AVDictionary of options - if err := audio.Open(nil); err != nil { - log.Fatalf("could not open audio codec: %v", err) - } - } - - fmt.Println(ctx) - - // Dump the output format - ff.AVFormat_dump_format(ctx, 0, *out) - - // Open the output file, if needed - if !ctx.Flags().Is(ff.AVFMT_NOFILE) { - w, err := ff.AVFormat_avio_open(*out, ff.AVIO_FLAG_WRITE) - if err != nil { - log.Fatalf("could not open output file: %v", err) - } else { - ctx.SetPb(w) - } - defer ff.AVFormat_avio_close(w) - } - - // Write the stream header, if any - // TODO: AVDictionary of options - if err := ff.AVFormat_write_header(ctx, nil); err != nil { - log.Fatalf("could not write header: %v", err) - } - - // TODO Write data - encode_audio, encode_video := true, true - for encode_audio || encode_video { - // Choose video if both are available, and video is earlier than audio - if (encode_video && !encode_audio) || (encode_video && ff.AVUtil_compare_ts(video.next_pts, video.Encoder.TimeBase(), audio.next_pts, audio.Encoder.TimeBase()) <= 0) { - fmt.Println("TODO: Write video frame") - encode_video = false - // encode_video = !write_video_frame(ctx, video) - } else { - fmt.Println("TODO: Write audio frame") - encode_audio = false - // encode_audio = !write_audio_frame(ctx, audio) - } - } - - // Write the trailer - if err := ff.AVFormat_write_trailer(ctx); err != nil { - log.Fatalf("could not write trailer: %v", err) - } -} - -/* - * Copyright (c) 2003 Fabrice Bellard - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL - * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ diff --git a/cmd/ffmpeg/mux/stream.go b/cmd/ffmpeg/mux/stream.go deleted file mode 100644 index 63ea0c9..0000000 --- a/cmd/ffmpeg/mux/stream.go +++ /dev/null @@ -1,256 +0,0 @@ -package main - -import ( - "encoding/json" - "errors" - "math" - - // Packages - ff "github.com/mutablelogic/go-media/sys/ffmpeg61" -) - -//////////////////////////////////////////////////////////////////////////////// - -// a wrapper around an output AVStream -type Stream struct { - // Main parameters - Codec *ff.AVCodec - Encoder *ff.AVCodecContext - Stream *ff.AVStream - - tmp_packet *ff.AVPacket - next_pts int64 // pts of the next frame that will be generated - samples_count int - frame *ff.AVFrame - tmp_frame *ff.AVFrame - packet *ff.AVPacket - t, tincr, tincr2 float64 - sws_ctx *ff.SWSContext - swr_ctx *ff.SWRContext -} - -func (stream *Stream) String() string { - data, _ := json.MarshalIndent(stream, "", " ") - return string(data) -} - -//////////////////////////////////////////////////////////////////////////////// - -// Create a new output stream, add it to the media context and initialize the codec. -func NewStream(ctx *ff.AVFormatContext, codec_id ff.AVCodecID) (*Stream, error) { - stream := &Stream{} - - // Codec - codec := ff.AVCodec_find_encoder(codec_id) - if codec == nil { - return nil, errors.New("could not find codec") - } else { - stream.Codec = codec - } - - // Packet - if packet := ff.AVCodec_packet_alloc(); packet == nil { - return nil, errors.New("could not allocate packet") - } else { - stream.tmp_packet = packet - } - - // Stream - if str := ff.AVFormat_new_stream(ctx, nil); str == nil { - ff.AVCodec_packet_free(stream.tmp_packet) - return nil, errors.New("could not allocate stream") - } else { - stream_id := int(ctx.NumStreams()) - stream.Stream = str - stream.Stream.SetId(stream_id) - } - - // Codec context - if encoder := ff.AVCodec_alloc_context(codec); encoder == nil { - ff.AVCodec_packet_free(stream.tmp_packet) - return nil, errors.New("could not allocate codec context") - } else { - stream.Encoder = encoder - } - - // Set parameters for the encoder - switch stream.Codec.Type() { - case ff.AVMEDIA_TYPE_AUDIO: - if fmts := stream.Codec.SampleFormats(); len(fmts) > 0 { - stream.Encoder.SetSampleFormat(fmts[0]) - } else { - stream.Encoder.SetSampleFormat(ff.AV_SAMPLE_FMT_FLTP) - } - if rates := stream.Codec.SupportedSamplerates(); len(rates) > 0 { - stream.Encoder.SetSampleRate(rates[0]) - } else { - stream.Encoder.SetSampleRate(44100) - } - stream.Encoder.SetBitRate(64000) - if err := stream.Encoder.SetChannelLayout(ff.AV_CHANNEL_LAYOUT_STEREO); err != nil { - ff.AVCodec_packet_free(stream.tmp_packet) - return nil, err - } - stream.Stream.SetTimeBase(ff.AVUtil_rational(1, stream.Encoder.SampleRate())) - case ff.AVMEDIA_TYPE_VIDEO: - stream.Encoder.SetBitRate(400000) - // Resolution must be a multiple of two. - stream.Encoder.SetWidth(352) - stream.Encoder.SetHeight(288) - /* timebase: This is the fundamental unit of time (in seconds) in terms - * of which frame timestamps are represented. For fixed-fps content, - * timebase should be 1/framerate and timestamp increments should be - * identical to 1. */ - stream.Stream.SetTimeBase(ff.AVUtil_rational(1, 25)) - stream.Encoder.SetTimeBase(stream.Stream.TimeBase()) - stream.Encoder.SetGopSize(12) /* emit one intra frame every twelve frames at most */ - stream.Encoder.SetPixFmt(ff.AV_PIX_FMT_YUV420P) - - if stream.Codec.ID() == ff.AV_CODEC_ID_MPEG2VIDEO { - /* just for testing, we also add B frames */ - stream.Encoder.SetMaxBFrames(2) - } - if stream.Codec.ID() == ff.AV_CODEC_ID_MPEG1VIDEO { - /* Needed to avoid using macroblocks in which some coeffs overflow. - * This does not happen with normal video, it just happens here as - * the motion of the chroma plane does not match the luma plane. */ - stream.Encoder.SetMbDecision(ff.FF_MB_DECISION_SIMPLE) - } - } - - // Some formats want stream headers to be separate - if ctx.Output().Flags().Is(ff.AVFMT_GLOBALHEADER) { - stream.Encoder.SetFlags(stream.Encoder.Flags() | ff.AV_CODEC_FLAG_GLOBAL_HEADER) - } - - // Return success - return stream, nil -} - -func (stream *Stream) Close() { - ff.AVCodec_packet_free(stream.tmp_packet) - ff.AVCodec_free_context(stream.Encoder) - ff.AVUtil_frame_free(stream.frame) - ff.AVUtil_frame_free(stream.tmp_frame) - ff.SWResample_free(stream.swr_ctx) -} - -func (stream *Stream) Open(opts *ff.AVDictionary) error { - // Create a copy of the opts - opt, err := ff.AVUtil_dict_copy(opts, 0) - if err != nil { - return err - } - defer ff.AVUtil_dict_free(opt) - - // Open the codec - if err := ff.AVCodec_open(stream.Encoder, stream.Codec, opt); err != nil { - return err - } - - switch stream.Codec.Type() { - case ff.AVMEDIA_TYPE_AUDIO: - stream.t = 0 - // increment frequency by 110 Hz per second - stream.tincr = 2 * math.Pi * 110.0 / float64(stream.Encoder.SampleRate()) - stream.tincr2 = 2 * math.Pi * 110.0 / float64(stream.Encoder.SampleRate()) / float64(stream.Encoder.SampleRate()) - - // Number of samples in a frame - nb_samples := stream.Encoder.FrameSize() - if stream.Codec.Capabilities().Is(ff.AV_CODEC_CAP_VARIABLE_FRAME_SIZE) { - nb_samples = 10000 - } - - if frame, err := alloc_audio_frame(stream.Encoder.SampleFormat(), stream.Encoder.ChannelLayout(), stream.Encoder.SampleRate(), nb_samples); err != nil { - return err - } else { - stream.frame = frame - } - if frame, err := alloc_audio_frame(ff.AV_SAMPLE_FMT_S16, stream.Encoder.ChannelLayout(), stream.Encoder.SampleRate(), nb_samples); err != nil { - return err - } else { - stream.tmp_frame = frame - } - // create resampler context - if swr_ctx := ff.SWResample_alloc(); swr_ctx == nil { - return errors.New("could not allocate resample context") - } else if err := ff.SWResample_set_opts(swr_ctx, - stream.Encoder.ChannelLayout(), stream.Encoder.SampleFormat(), stream.Encoder.SampleRate(), // out - stream.Encoder.ChannelLayout(), ff.AV_SAMPLE_FMT_S16, stream.Encoder.SampleRate(), // in - ); err != nil { - ff.SWResample_free(swr_ctx) - return err - } else if err := ff.SWResample_init(swr_ctx); err != nil { - ff.SWResample_free(swr_ctx) - return err - } else { - stream.swr_ctx = swr_ctx - } - case ff.AVMEDIA_TYPE_VIDEO: - // Allocate a re-usable frame - if frame, err := alloc_video_frame(stream.Encoder.PixFmt(), stream.Encoder.Width(), stream.Encoder.Height()); err != nil { - return err - } else { - stream.frame = frame - } - // If the output format is not YUV420P, then a temporary YUV420P picture is needed too. It is then converted to the required - // output format. - if stream.Encoder.PixFmt() != ff.AV_PIX_FMT_YUV420P { - if frame, err := alloc_video_frame(ff.AV_PIX_FMT_YUV420P, stream.Encoder.Width(), stream.Encoder.Height()); err != nil { - return err - } else { - stream.tmp_frame = frame - } - } - } - - // copy the stream parameters to the muxer - if err := ff.AVCodec_parameters_from_context(stream.Stream.CodecPar(), stream.Encoder); err != nil { - return err - } - - // Return success - return nil -} - -func alloc_video_frame(pix_fmt ff.AVPixelFormat, width, height int) (*ff.AVFrame, error) { - frame := ff.AVUtil_frame_alloc() - if frame == nil { - return nil, errors.New("could not allocate video frame") - } - frame.SetWidth(width) - frame.SetHeight(height) - frame.SetPixFmt(pix_fmt) - - // allocate the buffers for the frame data - if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil { - ff.AVUtil_frame_free(frame) - return nil, err - } - - // Return success - return frame, nil -} - -func alloc_audio_frame(sample_fmt ff.AVSampleFormat, channel_layout ff.AVChannelLayout, sample_rate, nb_samples int) (*ff.AVFrame, error) { - frame := ff.AVUtil_frame_alloc() - if frame == nil { - return nil, errors.New("could not allocate audio frame") - } - frame.SetSampleFormat(sample_fmt) - frame.SetSampleRate(sample_rate) - frame.SetNumSamples(nb_samples) - if err := frame.SetChannelLayout(channel_layout); err != nil { - ff.AVUtil_frame_free(frame) - return nil, err - } - - // allocate the buffers for the frame data - if err := ff.AVUtil_frame_get_buffer(frame, false); err != nil { - ff.AVUtil_frame_free(frame) - return nil, err - } - - // Return success - return frame, nil -} diff --git a/manager.go b/manager.go index b0a9c2c..10af4e6 100644 --- a/manager.go +++ b/manager.go @@ -1,6 +1,7 @@ package media import ( + "errors" "fmt" "io" "runtime" @@ -301,12 +302,12 @@ func (manager *manager) Read(r io.Reader, format Format, opts ...string) (Media, // Create a media file for writing, from a url, path, or device. func (manager *manager) Create(url string, format Format, metadata []Metadata, params ...Parameters) (Media, error) { - return createMedia(url, format, metadata, params...) + return nil, errors.New("not implemented") } // Create a media stream for writing. func (manager *manager) Write(w io.Writer, format Format, metadata []Metadata, params ...Parameters) (Media, error) { - return createWriter(w, format, metadata, params...) + return nil, errors.New("not implemented") } // Return version information for the media manager as a set of metadata diff --git a/writer.go b/writer.go deleted file mode 100644 index 4589824..0000000 --- a/writer.go +++ /dev/null @@ -1,320 +0,0 @@ -package media - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - - // Packages - ff "github.com/mutablelogic/go-media/sys/ffmpeg61" - - // Namespace imports - . "github.com/djthorpe/go-errors" -) - -//////////////////////////////////////////////////////////////////////////////// -// TYPES - -type writer struct { - t MediaType - output *ff.AVFormatContext - avio *ff.AVIOContextEx - metadata *ff.AVDictionary - header bool - encoder map[int]*encoder -} - -type writer_callback struct { - w io.Writer -} - -var _ Media = (*writer)(nil) - -//////////////////////////////////////////////////////////////////////////////// -// LIFECYCLE - -// Create media from a url or device -func createMedia(url string, format Format, metadata []Metadata, params ...Parameters) (*writer, error) { - writer := new(writer) - writer.t = OUTPUT - writer.encoder = make(map[int]*encoder, len(params)) - - // If there are no streams, then return an error - if len(params) == 0 { - return nil, ErrBadParameter.With("no streams specified for encoder") - } - - // Guess the output format - var ofmt *ff.AVOutputFormat - if format == nil && url != "" { - ofmt = ff.AVFormat_guess_format("", url, "") - } else if format != nil { - ofmt = format.(*outputformat).ctx - } - if ofmt == nil { - return nil, ErrBadParameter.With("unable to guess the output format") - } - - // Allocate the output media context - ctx, err := ff.AVFormat_create_file(url, ofmt) - if err != nil { - return nil, err - } else { - writer.output = ctx - } - - // Add encoders and streams - var result error - for i, param := range params { - // Stream Id from codec parameters, or use the index - stream_id := param.Id() - if stream_id <= 0 { - stream_id = i + 1 - } - encoder, err := newEncoder(ctx, stream_id, param) - if err != nil { - result = errors.Join(result, err) - } else if _, exists := writer.encoder[stream_id]; exists { - - } else { - writer.encoder[stream_id] = encoder - } - } - - // Return any errors from creating the streams - if result != nil { - return nil, errors.Join(result, writer.Close()) - } - - // Open the output file, if needed - if !ctx.Output().Flags().Is(ff.AVFMT_NOFILE) { - w, err := ff.AVFormat_avio_open(url, ff.AVIO_FLAG_WRITE) - if err != nil { - return nil, errors.Join(err, writer.Close()) - } else { - ctx.SetPb(w) - writer.avio = w - } - } - - // Set metadata - if len(metadata) > 0 { - writer.metadata = ff.AVUtil_dict_alloc() - if writer.metadata == nil { - return nil, errors.Join(errors.New("unable to allocate metadata dictionary"), writer.Close()) - } - for _, m := range metadata { - // Ignore duration and artwork fields - key := m.Key() - if key == MetaArtwork || key == MetaDuration { - continue - } - // Set dictionary entry - if err := ff.AVUtil_dict_set(writer.metadata, key, fmt.Sprint(m.Value()), ff.AV_DICT_APPEND); err != nil { - return nil, errors.Join(err, writer.Close()) - } - } - // TODO: Create artwork streams - } - - // Write the header - if err := ff.AVFormat_write_header(ctx, nil); err != nil { - return nil, errors.Join(err, writer.Close()) - } else { - writer.header = true - } - - // Return success - return writer, nil -} - -// Create media from io.Writer -// TODO -func createWriter(w io.Writer, format Format, metadata []Metadata, params ...Parameters) (*writer, error) { - return nil, ErrNotImplemented -} - -func (w *writer) Close() error { - var result error - - // Write the trailer if the header was written - if w.header { - if err := ff.AVFormat_write_trailer(w.output); err != nil { - result = errors.Join(result, err) - } - } - - // Close encoders - for _, encoder := range w.encoder { - result = errors.Join(result, encoder.Close()) - } - - // Free output resources - if w.output != nil { - // This calls avio_close(w.avio) - result = errors.Join(result, ff.AVFormat_close_writer(w.output)) - } - - // Free resources - if w.metadata != nil { - ff.AVUtil_dict_free(w.metadata) - } - - // Release resources - w.encoder = nil - w.metadata = nil - w.avio = nil - w.output = nil - - // Return any errors - return result -} - -//////////////////////////////////////////////////////////////////////////////// -// STRINGIFY - -// Display the reader as a string -func (w *writer) MarshalJSON() ([]byte, error) { - return json.Marshal(w.output) -} - -// Display the reader as a string -func (w *writer) String() string { - data, _ := json.MarshalIndent(w, "", " ") - return string(data) -} - -//////////////////////////////////////////////////////////////////////////////// -// PUBLIC METHODS - -func (w *writer) Decoder(DecoderMapFunc) (Decoder, error) { - return nil, ErrOutOfOrder.With("not an input stream") -} - -func (w *writer) Mux(ctx context.Context, fn MuxFunc) error { - // Check fn - if fn == nil { - return ErrBadParameter.With("nil mux function") - } - - // Create a new map of encoders - encoders := make(map[int]*encoder, len(w.encoder)) - for k, v := range w.encoder { - encoders[k] = v - } - -FOR_LOOP: - for { - select { - case <-ctx.Done(): - break FOR_LOOP - default: - // Loop until no more encoders are available to send packets - if len(encoders) == 0 { - break FOR_LOOP - } - - // Find the first encoder which should return a packet - var next_encoder *encoder - var next_stream int - for stream, encoder := range encoders { - // Initialise the next encoder - if next_encoder == nil { - next_encoder = encoder - next_stream = stream - continue - } - // Compare - if !compareNextPts(next_encoder, encoder) { - next_encoder = encoder - next_stream = stream - } - } - - // Get a packet from the encoder - packet, err := next_encoder.encode(fn) - if errors.Is(err, io.EOF) { - break FOR_LOOP - } else if err != nil { - return err - } else if packet == nil { - // Remove the encoder from the map - delete(encoders, next_stream) - continue FOR_LOOP - } - - // Send the packet to the muxer - //av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base); - // Packet's stream_index field must be set to the index of the corresponding stream in s->streams. - // The timestamps (pts, dts) must be set to correct values in the stream's timebase - // (unless the output format is flagged with the AVFMT_NOTIMESTAMPS flag, then they can be set - // to AV_NOPTS_VALUE). The dts for subsequent packets in one stream must be strictly increasing - // (unless the output format is flagged with the AVFMT_TS_NONSTRICT, then they merely have to - // be nondecreasing). duration should also be set if known. - if err := ff.AVCodec_interleaved_write_frame(w.output, packet); err != nil { - return err - } - } - } - - // Flush - if err := ff.AVCodec_interleaved_write_frame(w.output, nil); err != nil { - return err - } - - // Return the context error, which will be nil if the loop ended normally - return ctx.Err() -} - -// Returns true if a.next_pts is greater than b.next_pts -func compareNextPts(a, b *encoder) bool { - return ff.AVUtil_compare_ts(a.next_pts, a.stream.TimeBase(), b.next_pts, b.stream.TimeBase()) > 0 -} - -/* - while (1) { - AVStream *in_stream, *out_stream; - - ret = av_read_frame(ifmt_ctx, pkt); - if (ret < 0) - break; - - in_stream = ifmt_ctx->streams[pkt->stream_index]; - if (pkt->stream_index >= stream_mapping_size || - stream_mapping[pkt->stream_index] < 0) { - av_packet_unref(pkt); - continue; - } - - pkt->stream_index = stream_mapping[pkt->stream_index]; - out_stream = ofmt_ctx->streams[pkt->stream_index]; - log_packet(ifmt_ctx, pkt, "in"); - - // copy packet - av_packet_rescale_ts(pkt, in_stream->time_base, out_stream->time_base); - pkt->pos = -1; - log_packet(ofmt_ctx, pkt, "out"); - - ret = av_interleaved_write_frame(ofmt_ctx, pkt); - // pkt is now blank (av_interleaved_write_frame() takes ownership of - // its contents and resets pkt), so that no unreferencing is necessary. - // This would be different if one used av_write_frame(). - if (ret < 0) { - fprintf(stderr, "Error muxing packet\n"); - break; - } - } -*/ - -// Return OUTPUT and combination of DEVICE and STREAM -func (w *writer) Type() MediaType { - return w.t -} - -// Return the metadata for the media. -func (w *writer) Metadata(...string) []Metadata { - // Not yet implemented - return nil -} diff --git a/writer_test.go b/writer_test.go deleted file mode 100644 index 5e21d43..0000000 --- a/writer_test.go +++ /dev/null @@ -1,84 +0,0 @@ -//go:build !container - -package media_test - -// TODO: Allow this test to run in containers - -import ( - "context" - "path/filepath" - "strings" - "testing" - - // Package imports - "github.com/stretchr/testify/assert" - - // Namespace imports - . "github.com/mutablelogic/go-media" -) - -func Test_writer_001(t *testing.T) { - assert := assert.New(t) - manager, err := NewManager(OptLog(true, func(v string) { - t.Log(strings.TrimSpace(v)) - })) - if !assert.NoError(err) { - t.SkipNow() - } - - // Write audio file - filename := filepath.Join(t.TempDir(), t.Name()+".mp3") - stream, err := manager.AudioParameters("mono", "fltp", 22050) - if !assert.NoError(err) { - t.SkipNow() - } - - writer, err := manager.Create(filename, nil, nil, stream) - if !assert.NoError(err) { - t.SkipNow() - } - defer writer.Close() - - t.Log(writer, "=>", filename) - - // Perform muxing of packets - writer.Mux(context.Background(), func(stream int) (Packet, error) { - t.Log("Muxing packet for stream", stream) - return nil, nil - }) -} - -func Test_writer_002(t *testing.T) { - assert := assert.New(t) - manager, err := NewManager(OptLog(true, func(v string) { - t.Log(strings.TrimSpace(v)) - })) - if !assert.NoError(err) { - t.SkipNow() - } - - // Write file with both audio and video - filename := filepath.Join(t.TempDir(), t.Name()+".mp4") - audio, err := manager.AudioParameters("mono", "fltp", 22050) - if !assert.NoError(err) { - t.SkipNow() - } - video, err := manager.VideoParameters(1280, 720, "yuv420p") - if !assert.NoError(err) { - t.SkipNow() - } - - writer, err := manager.Create(filename, nil, nil, audio, video) - if !assert.NoError(err) { - t.SkipNow() - } - defer writer.Close() - - t.Log(writer, "=>", filename) - - // Perform muxing of packets - writer.Mux(context.Background(), func(stream int) (Packet, error) { - t.Log("Muxing packet for stream", stream) - return nil, nil - }) -} From 252ef43f6202bec44ff46fc4c0a2510938cfa457 Mon Sep 17 00:00:00 2001 From: David Thorpe Date: Sun, 30 Jun 2024 19:50:20 +0200 Subject: [PATCH 2/2] Some more fixes --- go.mod | 1 + manager.go | 29 +--------------------- manager_test.go | 14 ----------- pkg/ffmpeg/rescaler_test.go | 2 +- pkg/generator/yuv420p_test.go | 6 ++--- pkg/version/version.go | 45 +++++++++++++++++++++++++++++++++++ 6 files changed, 51 insertions(+), 46 deletions(-) diff --git a/go.mod b/go.mod index f0e0ff2..56f2997 100755 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/djthorpe/go-tablewriter v0.0.8 github.com/mutablelogic/go-client v1.0.8 github.com/stretchr/testify v1.9.0 + golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 ) require ( diff --git a/manager.go b/manager.go index 10af4e6..bf1373f 100644 --- a/manager.go +++ b/manager.go @@ -4,10 +4,8 @@ import ( "errors" "fmt" "io" - "runtime" // Package imports - version "github.com/mutablelogic/go-media/pkg/version" ff "github.com/mutablelogic/go-media/sys/ffmpeg61" ) @@ -312,32 +310,7 @@ func (manager *manager) Write(w io.Writer, format Format, metadata []Metadata, p // Return version information for the media manager as a set of metadata func (manager *manager) Version() []Metadata { - metadata := []Metadata{ - newMetadata("libavcodec_version", ffVersionAsString(ff.AVCodec_version())), - newMetadata("libavformat_version", ffVersionAsString(ff.AVFormat_version())), - newMetadata("libavutil_version", ffVersionAsString(ff.AVUtil_version())), - newMetadata("libavdevice_version", ffVersionAsString(ff.AVDevice_version())), - // newMetadata("libavfilter_version", ff.AVFilter_version()), - newMetadata("libswscale_version", ffVersionAsString(ff.SWScale_version())), - newMetadata("libswresample_version", ffVersionAsString(ff.SWResample_version())), - } - if version.GitSource != "" { - metadata = append(metadata, newMetadata("git_source", version.GitSource)) - } - if version.GitBranch != "" { - metadata = append(metadata, newMetadata("git_branch", version.GitBranch)) - } - if version.GitTag != "" { - metadata = append(metadata, newMetadata("git_tag", version.GitTag)) - } - if version.GoBuildTime != "" { - metadata = append(metadata, newMetadata("go_build_time", version.GoBuildTime)) - } - if runtime.Version() != "" { - metadata = append(metadata, newMetadata("go_version", runtime.Version())) - metadata = append(metadata, newMetadata("go_arch", runtime.GOOS+"/"+runtime.GOARCH)) - } - return metadata + return nil } // Log error messages diff --git a/manager_test.go b/manager_test.go index 37f1994..9bf3a8e 100644 --- a/manager_test.go +++ b/manager_test.go @@ -39,20 +39,6 @@ func Test_manager_002(t *testing.T) { t.Log(formats) } -func Test_manager_003(t *testing.T) { - assert := assert.New(t) - - manager, err := NewManager() - if !assert.NoError(err) { - t.SkipNow() - } - - version := manager.Version() - assert.NotNil(version) - - tablewriter.New(os.Stderr, tablewriter.OptHeader(), tablewriter.OptOutputText()).Write(version) -} - func Test_manager_004(t *testing.T) { assert := assert.New(t) diff --git a/pkg/ffmpeg/rescaler_test.go b/pkg/ffmpeg/rescaler_test.go index 46930e3..e90307e 100644 --- a/pkg/ffmpeg/rescaler_test.go +++ b/pkg/ffmpeg/rescaler_test.go @@ -53,7 +53,7 @@ func Test_rescaler_002(t *testing.T) { assert := assert.New(t) // Create an image generator - image, err := generator.NewYUV420P(ffmpeg.VideoPar("yuva420p", "1280x720", 25)) + image, err := generator.NewYUV420P(ffmpeg.VideoPar("yuv420p", "1280x720", 25)) if !assert.NoError(err) { t.FailNow() } diff --git a/pkg/generator/yuv420p_test.go b/pkg/generator/yuv420p_test.go index 8d037d1..32b46da 100644 --- a/pkg/generator/yuv420p_test.go +++ b/pkg/generator/yuv420p_test.go @@ -14,7 +14,7 @@ import ( func Test_yuv420p_001(t *testing.T) { assert := assert.New(t) - image, err := generator.NewYUV420P(25, ffmpeg.VideoPar("yuva420p", "1280x720")) + image, err := generator.NewYUV420P(ffmpeg.VideoPar("yuv420p", "1280x720", 25)) if !assert.NoError(err) { t.FailNow() } @@ -25,7 +25,7 @@ func Test_yuv420p_001(t *testing.T) { func Test_yuv420p_002(t *testing.T) { assert := assert.New(t) - image, err := generator.NewYUV420P(25, ffmpeg.VideoPar("yuva420p", "1280x720")) + image, err := generator.NewYUV420P(ffmpeg.VideoPar("yuv420p", "1280x720", 25)) if !assert.NoError(err) { t.FailNow() } @@ -39,7 +39,7 @@ func Test_yuv420p_002(t *testing.T) { func Test_yuv420p_003(t *testing.T) { assert := assert.New(t) - image, err := generator.NewYUV420P(25, ffmpeg.VideoPar("yuva420p", "1280x720")) + image, err := generator.NewYUV420P(ffmpeg.VideoPar("yuv420p", "1280x720", 25)) if !assert.NoError(err) { t.FailNow() } diff --git a/pkg/version/version.go b/pkg/version/version.go index cc3e270..cb16017 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,8 +1,53 @@ package version +import ( + "fmt" + "runtime" + + ffmpeg "github.com/mutablelogic/go-media/pkg/ffmpeg" + ff "github.com/mutablelogic/go-media/sys/ffmpeg61" +) + var ( GitSource string GitTag string GitBranch string GoBuildTime string ) + +// Return version information as a set of metadata +func Version() []*ffmpeg.Metadata { + metadata := []*ffmpeg.Metadata{ + ffmpeg.NewMetadata("libavcodec_version", ffVersionAsString(ff.AVCodec_version())), + ffmpeg.NewMetadata("libavformat_version", ffVersionAsString(ff.AVFormat_version())), + ffmpeg.NewMetadata("libavutil_version", ffVersionAsString(ff.AVUtil_version())), + ffmpeg.NewMetadata("libavdevice_version", ffVersionAsString(ff.AVDevice_version())), + // newMetadata("libavfilter_version", ff.AVFilter_version()), + ffmpeg.NewMetadata("libswscale_version", ffVersionAsString(ff.SWScale_version())), + ffmpeg.NewMetadata("libswresample_version", ffVersionAsString(ff.SWResample_version())), + } + if GitSource != "" { + metadata = append(metadata, ffmpeg.NewMetadata("git_source", GitSource)) + } + if GitBranch != "" { + metadata = append(metadata, ffmpeg.NewMetadata("git_branch", GitBranch)) + } + if GitTag != "" { + metadata = append(metadata, ffmpeg.NewMetadata("git_tag", GitTag)) + } + if GoBuildTime != "" { + metadata = append(metadata, ffmpeg.NewMetadata("go_build_time", GoBuildTime)) + } + if runtime.Version() != "" { + metadata = append(metadata, ffmpeg.NewMetadata("go_version", runtime.Version())) + metadata = append(metadata, ffmpeg.NewMetadata("go_arch", runtime.GOOS+"/"+runtime.GOARCH)) + } + return metadata +} + +//////////////////////////////////////////////////////////////////////////// +// PRIVATE METHODS + +func ffVersionAsString(version uint) string { + return fmt.Sprintf("%d.%d.%d", version&0xFF0000>>16, version&0xFF00>>8, version&0xFF) +}